Consolida rastreabilidade territorial e plano autenticado V2

This commit is contained in:
2026-10-09 02:05:44 +02:00
parent 30acdc9934
commit 6a71aa47e3
33 changed files with 2555 additions and 49 deletions
+7
View File
@@ -1,5 +1,12 @@
# Regras do acervo documental InvestMoz
## Aceitação das fontes oficiais — decisão do responsável, 08/10/2026
- Dados e documentos com origem oficial/governamental identificada (incluindo BR, decretos e leis) são aceites pelo responsável como fontes fidedignas e válidas. Não exigir segunda ou terceira validação da autenticidade/proveniência oficial, nem certificação por outra fonte.
- Registar e recuperar origem, exemplar, versão e citação é rastreabilidade; a ausência desses campos num cadastro não revoga a aceitação nem torna o dado inválido. Preservar as validações e decisões já documentadas.
- Esta decisão tem precedência sobre pendências genéricas de revalidação de proveniência oficial em relatórios históricos. Não estender a classificação oficial a documentos de promotores ou de programação sem origem oficial identificada.
- A aceitação da fonte não substitui a identificação do âmbito, período aplicável, alterações/revogações expressas no próprio acervo oficial ou dos parâmetros necessários a uma regra/cálculo. Conferir esses elementos no documento oficial é aplicação da fonte aceite, não uma segunda validação da sua credibilidade. Não ativar cálculos por importação/publicação.
- O responsável define o acervo e a finalidade. Inventariar todos os ficheiros e anexos acessíveis antes de seleccionar documentos para consulta; não omitir por não serem usados por uma funcionalidade.
- Manter três estados independentes: disponibilidade para consulta, situação jurídica/documental e utilização numa regra ou cálculo. Importação/publicação nunca activa um cálculo fiscal.
- Não usar associação a módulo, requisito ou artigo como pré-condição para cadastrar ou consultar um documento autorizado. Relações sem citação verificada ficam pendentes e visivelmente identificadas.
@@ -3,21 +3,37 @@
namespace App\Http\Controllers;
use App\Services\InvestmentRelationshipRepair;
use App\Services\InvestmentTerritorialTraceability;
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 InvestmentRelationshipRepairController extends Controller
{
public function preview(InvestmentRelationshipRepair $repair): Response
{
return Inertia::render('management/relationship-repair', $repair->preview());
return Inertia::render('management/relationship-repair', [...$repair->preview(), 'traceability' => app(InvestmentTerritorialTraceability::class)->preview()]);
}
public function apply(Request $request, InvestmentRelationshipRepair $repair): RedirectResponse|JsonResponse
{
if ($request->input('operation') === 'traceability') {
$data = $request->validate(['hash' => ['required', 'string', 'size:64'], 'reverse' => ['required', 'boolean']]);
try {
$result = app(InvestmentTerritorialTraceability::class)->apply($data['hash'], $request->user()->id, $data['reverse']);
} catch (HttpException $exception) {
if ($request->expectsJson()) {
throw $exception;
}
return back()->withErrors(['operation' => $exception->getMessage()]);
}
return $request->expectsJson() ? response()->json($result) : back()->with('success', $data['reverse'] ? 'Rastreabilidade revertida, histórico preservado.' : 'Rastreabilidade aplicada; validações e dados territoriais preservados.');
}
$data = $request->validate(['hash' => ['required', 'string', 'size:64'], 'row_id' => ['nullable', 'string', 'size:64'],
'validation_hash' => ['required_with:row_id', 'nullable', 'string', 'size:64']]);
$result = $repair->apply($data['hash'], $request->user()->id, $data['row_id'] ?? null, $data['validation_hash'] ?? null);
@@ -7,6 +7,7 @@ use App\Models\InvestmentZone;
use App\Models\Opportunity;
use App\Services\InvestmentCatalogue;
use App\Services\InvestmentGeography;
use App\Services\InvestmentTerritorialTraceability;
use App\Services\InvestmentZoneAccess;
use App\Services\InvestmentZones;
use Illuminate\Http\JsonResponse;
@@ -73,6 +74,7 @@ class InvestmentZoneController extends Controller
'operators' => $management ? DB::table('organizations')->whereNull('deleted_at')->orderBy('name')->limit(200)->get(['id', 'name']) : [],
'links' => $management ? DB::table('investment_zone_links')->where('zone_id', $id)->orderByDesc('created_at')->limit(100)->get() : [],
'locations' => $locations,
'territorialSources' => app(InvestmentTerritorialTraceability::class)->sources('investment_zones', $id),
'attachments' => $attachments,
'revisions' => $management ? DB::table('investment_zone_revisions')->where('zone_id', $id)->orderByDesc('version_no')->limit(20)->get(['version_no', 'action', 'sha256', 'created_at']) : []];
+60 -17
View File
@@ -20,7 +20,7 @@ class InvestmentCatalogue
$filters = $request->validate(['q' => ['nullable', 'string', 'max:120'], 'sector' => ['nullable', 'string', 'max:80'],
'opportunity_id' => ['nullable', 'integer', 'min:1'],
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid'],
'include_national' => ['nullable', 'boolean'],
'include_national' => ['nullable', 'boolean'], 'territory_basis' => ['nullable', 'in:all,direct,context'],
'province' => ['nullable', 'string', 'max:100'], 'district' => ['nullable', 'string', 'max:100'],
'cluster_id' => ['nullable', 'uuid'], 'corridor_id' => ['nullable', 'uuid'], 'project_id' => ['nullable', 'uuid'],
'geo_location_id' => ['nullable', 'uuid'], 'education' => ['nullable', 'boolean'],
@@ -44,6 +44,13 @@ class InvestmentCatalogue
public function query(array $filters): Builder
{
$query = Opportunity::published()->with('sector:id,name,slug');
if (($filters['territory_basis'] ?? 'all') === 'direct') {
$query->where(fn ($q) => $q->whereNotNull('opportunities.primary_territory_id')->orWhereExists(fn ($l) => $l->selectRaw('1')->from('opportunity_locations')->whereColumn('opportunity_id', 'opportunities.id')));
} elseif (($filters['territory_basis'] ?? 'all') === 'context') {
$query->whereNull('opportunities.primary_territory_id')->whereNotExists(fn ($l) => $l->selectRaw('1')->from('opportunity_locations')->whereColumn('opportunity_id', 'opportunities.id'));
$query->where(fn ($q) => $q->whereExists(fn ($l) => $l->selectRaw('1')->from('opportunity_projects')->whereColumn('opportunity_id', 'opportunities.id')->whereIn('project_id', app(InvestmentRelations::class)->projectIds()))
->orWhereExists(fn ($l) => $l->selectRaw('1')->from('opportunity_clusters')->whereColumn('opportunity_id', 'opportunities.id')->whereIn('cluster_id', $this->publicStructureIds('clusters', 'cluster_id'))));
}
if (! empty($filters['opportunity_id'])) {
$query->whereKey($filters['opportunity_id']);
}
@@ -51,11 +58,13 @@ class InvestmentCatalogue
$includeNational = filter_var($filters['include_national'] ?? false, FILTER_VALIDATE_BOOLEAN);
if ($path !== []) {
$ids = app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id);
$query->where(function ($territory) use ($ids, $includeNational): void {
$query->where(function ($territory) use ($ids, $includeNational, $filters): void {
$territory->whereIn('opportunities.primary_territory_id', $ids)
->orWhereExists(fn ($locations) => $locations->selectRaw('1')->from('opportunity_locations as territory_link')
->whereColumn('territory_link.opportunity_id', 'opportunities.id')->whereIn('territory_link.geo_location_id', $ids));
app(InvestmentRelations::class)->opportunityTerritories($territory, $ids);
if (($filters['territory_basis'] ?? 'all') !== 'direct') {
app(InvestmentRelations::class)->opportunityTerritories($territory, $ids);
}
if ($includeNational) {
$territory->orWhere('opportunities.geography_scope', 'national');
}
@@ -86,6 +95,10 @@ class InvestmentCatalogue
$term = '%'.$filters['q'].'%';
$query->where(fn ($q) => $q->where('title', 'ilike', $term)->orWhere('summary', 'ilike', $term)->orWhere('reference', 'ilike', $term)
->orWhere('province', 'ilike', $term)->orWhereHas('sector', fn ($s) => $s->where('name', 'ilike', $term))
->orWhereExists(fn ($g) => $g->selectRaw('1')->from('geo_locations as search_location')->whereNull('search_location.deleted_at')->where('search_location.data_classification', 'public')
->where('search_location.name', 'ilike', $term)->whereIn('search_location.id', app(InvestmentGeography::class)->publicReferences()->select('id'))
->where(fn ($direct) => $direct->whereColumn('search_location.id', 'opportunities.primary_territory_id')
->orWhereExists(fn ($l) => $l->selectRaw('1')->from('opportunity_locations as search_link')->whereColumn('search_link.opportunity_id', 'opportunities.id')->whereColumn('search_link.geo_location_id', 'search_location.id'))))
->orWhereExists(fn ($d) => $d->selectRaw('1')->from('investment_dossiers as searchable_dossier')
->whereNull('searchable_dossier.deleted_at')->where('searchable_dossier.publication_status', 'published')
->where(fn ($text) => $text->where('searchable_dossier.title', 'ilike', $term)->orWhereRaw('searchable_dossier.content::text ILIKE ?', [$term]))
@@ -107,13 +120,15 @@ class InvestmentCatalogue
? app(InvestmentGeography::class)->descendants((string) $filters[$foreign])
: null;
if ($relation === 'locations') {
$query->where(function ($matches) use ($locationIds) {
$query->where(function ($matches) use ($locationIds, $filters) {
$matches->whereIn('opportunities.primary_territory_id', $locationIds ?? [])
->orWhereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_locations as link')
->join('geo_locations as target', 'target.id', '=', 'link.geo_location_id')
->whereColumn('link.opportunity_id', 'opportunities.id')->whereIn('link.geo_location_id', $locationIds ?? [])
->whereNull('target.deleted_at')->where('target.data_classification', 'public'));
app(InvestmentRelations::class)->opportunityTerritories($matches, $locationIds ?? []);
if (($filters['territory_basis'] ?? 'all') !== 'direct') {
app(InvestmentRelations::class)->opportunityTerritories($matches, $locationIds ?? []);
}
});
continue;
@@ -229,6 +244,8 @@ class InvestmentCatalogue
->join('investment_zones as z', 'z.id', '=', 'zl.zone_id')->where('m.opportunity_id', $id)
->get(['z.id', 'z.name', 'z.category', 'zl.association_type', 'zl.source_reference', 'zl.valid_from', 'zl.valid_until']);
$result['primary_territory_path'] = app(InvestmentGeography::class)->path(DB::table('opportunities')->where('id', $id)->value('primary_territory_id'));
$result['opportunity_locations'] = app(InvestmentRelations::class)->opportunityLocations($id, $management);
$result['source_evidence'] = app(InvestmentTerritorialTraceability::class)->sources('opportunities', $id);
return $result;
}
@@ -242,8 +259,11 @@ class InvestmentCatalogue
$records = $this->query($filters)->limit(1000)->get();
$total = $this->query($filters)->reorder()->count();
$ids = $records->pluck('id');
$points = DB::table('opportunities as o')->join('geo_locations as g', function ($join) {
$spatial = DB::table('opportunities as o')->join('geo_locations as g', function ($join) use ($filters) {
$join->on('g.id', '=', 'o.primary_territory_id')->orWhereExists(fn ($q) => $q->selectRaw('1')->from('opportunity_locations as l')->whereColumn('l.opportunity_id', 'o.id')->whereColumn('l.geo_location_id', 'g.id'));
if (($filters['territory_basis'] ?? 'all') === 'direct') {
return;
}
$join->orWhereExists(fn ($q) => $q->selectRaw('1')->from('opportunity_projects as map_project')
->join('projects as p', 'p.id', '=', 'map_project.project_id')->whereColumn('map_project.opportunity_id', 'o.id')->whereNull('p.deleted_at')
->whereNotIn('p.data_classification', ['confidential', 'restricted'])->whereIn('p.id', app(InvestmentRelations::class)->projectIds())
@@ -253,21 +273,31 @@ class InvestmentCatalogue
->whereNull('map_location.deleted_at')->where('map_location.data_classification', 'public')->where('map_location.role', '!=', 'impact'))));
})->join('geo_geometries as shape', 'shape.geo_location_id', '=', 'g.id')
->whereIn('o.id', $ids)->where('g.data_classification', 'public')->where('shape.data_classification', 'public')
->where('g.location_type', 'site')->whereNull('g.deleted_at')->whereNull('shape.deleted_at')->where('shape.geometry_type', 'point')
->whereNull('g.deleted_at')->whereNull('shape.deleted_at')
->whereRaw("NOT EXISTS (WITH RECURSIVE parents AS (
SELECT id,parent_id,deleted_at,data_classification,ARRAY[id] AS visited,false AS cycle FROM geo_locations WHERE id=g.id
UNION ALL SELECT p.id,p.parent_id,p.deleted_at,p.data_classification,a.visited||p.id,p.id=ANY(a.visited)
FROM geo_locations p JOIN parents a ON p.id=a.parent_id WHERE NOT a.cycle
) SELECT 1 FROM parents WHERE deleted_at IS NOT NULL OR data_classification<>'public' OR cycle)")
->whereRaw('ST_IsValid(shape.geom) AND NOT ST_IsEmpty(shape.geom) AND ST_SRID(shape.geom)=4326')
->whereRaw('ST_IsValid(shape.geom) AND NOT ST_IsEmpty(shape.geom) AND ST_SRID(shape.geom)=4326');
$points = (clone $spatial)->where('g.location_type', 'site')->where('shape.geometry_type', 'point')
->whereRaw('ST_X(shape.geom) BETWEEN -180 AND 180 AND ST_Y(shape.geom) BETWEEN -90 AND 90')
->orderBy('o.id')->orderByRaw("CASE WHEN g.source_system='investment-site-editor' THEN 0 ELSE 1 END")->orderBy('g.id')->orderBy('shape.id')
->get(['o.id', 'g.name', 'g.id as location_id', DB::raw('ST_AsGeoJSON(shape.geom) as geometry'),
->get(['o.id', 'g.name', 'g.id as location_id', 'shape.source_system', 'shape.properties', DB::raw('ST_AsGeoJSON(shape.geom) as geometry'),
DB::raw("CASE WHEN o.primary_territory_id=g.id OR EXISTS (SELECT 1 FROM opportunity_locations direct WHERE direct.opportunity_id=o.id AND direct.geo_location_id=g.id) THEN 'opportunity_site' ELSE 'linked_project_site' END as location_basis")])
->groupBy('id')->map(function ($candidates) {
$direct = $candidates->where('location_basis', 'opportunity_site');
$selected = $direct->isNotEmpty() ? $direct : $candidates;
// Several sites are legitimate; only conflicting coordinates for the same site are withheld.
$sites = [];
foreach ($candidates->groupBy('location_id') as $site) {
if ($site->unique('geometry')->count() === 1) {
foreach ($site as $candidate) {
$sites[] = $candidate;
break;
}
}
}
return $selected->unique('location_id')->count() === 1 && $selected->unique('geometry')->count() === 1 ? $selected->first() : null;
return $sites;
})->filter();
$relations = [];
foreach (['clusters', 'corridors', 'projects'] as $relation) {
@@ -278,22 +308,35 @@ class InvestmentCatalogue
->get(['l.opportunity_id', 't.name'])->groupBy('opportunity_id');
}
$features = $missing = [];
$areas = (clone $spatial)->whereIn('shape.geometry_type', ['polygon', 'multipolygon'])->orderBy('shape.id')
->get(['o.id', 'o.title', 'o.slug', 'g.name', 'shape.source_system', 'shape.properties', DB::raw('ST_AsGeoJSON(shape.geom) as geometry'),
DB::raw("CASE WHEN o.primary_territory_id=g.id OR EXISTS (SELECT 1 FROM opportunity_locations direct WHERE direct.opportunity_id=o.id AND direct.geo_location_id=g.id) THEN 'território declarado da oportunidade' ELSE 'contexto do projeto associado; implantação da oportunidade por confirmar' END AS basis")])
->map(fn ($area) => ['geometry' => json_decode($area->geometry, true), 'title' => $area->title.' — '.$area->name.' ('.$area->basis.')',
'source_reference' => $area->source_system, 'source_date' => json_decode($area->properties ?? '{}', true)['source_date'] ?? null,
'url' => '/oportunidades/'.$area->slug])->all();
$located = 0;
foreach ($records as $record) {
$properties = ['id' => $record->id, 'title' => $record->title, 'category' => $record->sector?->name,
'location' => trim($record->province.' '.$record->district), 'url' => '/oportunidades/'.$record->slug];
foreach ($relations as $relation => $groups) {
$properties[$relation] = ($groups->get($record->id) ?? collect())->pluck('name')->unique()->values()->all();
}
if ($point = $points->get($record->id)) {
$features[] = ['type' => 'Feature', 'geometry' => json_decode($point->geometry, true), 'properties' => [...$properties,
'location_basis' => $point->location_basis, 'location' => trim($properties['location'].' — '.$point->name).($point->location_basis === 'linked_project_site' ? ' (local documentado do projecto associado; implantação da oportunidade por confirmar)' : '')]];
$sites = collect($points->get($record->id) ?? []);
if ($sites->isNotEmpty()) {
$located++;
foreach ($sites as $point) {
$features[] = ['type' => 'Feature', 'geometry' => json_decode($point->geometry, true), 'properties' => [...$properties,
'location_id' => $point->location_id, 'source_reference' => $point->source_system,
'source_date' => json_decode($point->properties ?? '{}', true)['source_date'] ?? null,
'location_basis' => $point->location_basis, 'location' => $point->name.($point->location_basis === 'linked_project_site' ? ' (local do projecto associado; contexto territorial)' : ' (implantação cadastrada da oportunidade)')]];
}
} else {
$missing[] = [...$properties, 'reason' => 'missing_public_site_coordinates'];
}
}
return ['type' => 'FeatureCollection', 'limit' => 1000, 'matched_count' => $total, 'truncated' => $total > $records->count(),
'georeferenced_count' => count($features), 'unlocated' => $missing, 'features' => $features,
'complete' => $total === count($features)];
'georeferenced_count' => $located, 'marker_count' => count($features), 'unlocated' => $missing, 'features' => $features,
'areas' => $areas, 'complete' => $total === $located];
}
}
+27 -3
View File
@@ -11,6 +11,21 @@ use Illuminate\Support\Facades\DB;
/** Resolve documented relationships without generating geography or spreading opportunity memberships. */
class InvestmentRelations
{
/** Directly declared opportunity territories; a link does not certify implantation.
* @return list<array<string,mixed>>
*/
public function opportunityLocations(int $id, bool $management = false): array
{
$record = DB::table('opportunities')->where('id', $id)->first();
$items = DB::table('opportunity_locations')->where('opportunity_id', $id)->pluck('geo_location_id')
->map(fn ($location) => ['id' => $location, 'role' => 'opportunity', 'source_reference' => null, 'source_date' => null])->values()->all();
if ($record?->primary_territory_id && ! collect($items)->contains('id', $record->primary_territory_id)) {
$items[] = ['id' => $record->primary_territory_id, 'role' => 'opportunity', 'source_reference' => null, 'source_date' => null];
}
return $this->locations(array_values($items), $management);
}
public function projectIds(): Builder
{
return InvestmentPortfolioController::visible()->whereNotNull('project_id')->select('project_id')->toBase();
@@ -82,10 +97,18 @@ class InvestmentRelations
$shapes = DB::table('geo_geometries')->where('geo_location_id', $item['id'])->whereNull('deleted_at')
->when(! $management, fn ($q) => $q->where('data_classification', 'public'))
->where('geometry_type', 'point')->whereRaw('ST_IsValid(geom) AND NOT ST_IsEmpty(geom) AND ST_SRID(geom)=4326')
->orderBy('id')->get(['id', 'source_system', DB::raw('ST_AsGeoJSON(geom) as geometry')]);
->whereRaw('ST_X(geom) BETWEEN -180 AND 180 AND ST_Y(geom) BETWEEN -90 AND 90')
->orderBy('id')->get(['id', 'source_system', 'properties', DB::raw('ST_AsGeoJSON(geom) as geometry')]);
$coordinates = $shapes->map(fn ($shape) => ['geometry_id' => $shape->id, 'source_reference' => $shape->source_system,
'source_date' => json_decode($shape->properties ?? '{}', true)['source_date'] ?? null,
'geometry' => json_decode($shape->geometry, true)])->all();
$result[] = [...$item, 'name' => $site->name, 'location_type' => $site->location_type, 'path' => $path, 'coordinates' => $coordinates];
$boundaries = DB::table('geo_geometries')->where('geo_location_id', $item['id'])->whereNull('deleted_at')
->when(! $management, fn ($q) => $q->where('data_classification', 'public'))
->whereIn('geometry_type', ['polygon', 'multipolygon'])->whereRaw('ST_IsValid(geom) AND NOT ST_IsEmpty(geom) AND ST_SRID(geom)=4326')
->get(['id', 'source_system', 'properties', DB::raw('ST_AsGeoJSON(geom) as geometry')])
->map(fn ($shape) => ['geometry_id' => $shape->id, 'source_reference' => $shape->source_system,
'source_date' => json_decode($shape->properties ?? '{}', true)['source_date'] ?? null, 'geometry' => json_decode($shape->geometry, true)])->all();
$result[] = [...$item, 'name' => $site->name, 'location_type' => $site->location_type, 'path' => $path, 'coordinates' => $coordinates, 'boundaries' => $boundaries];
}
return $result;
@@ -94,7 +117,8 @@ class InvestmentRelations
/** @return array<string,mixed> */
public function dossier(InvestmentDossier $dossier, bool $management = false): array
{
$result = ['locations' => [], 'members' => [], 'clusters' => [], 'chainLinks' => [], 'chains' => []];
$result = ['locations' => [], 'members' => [], 'clusters' => [], 'chainLinks' => [], 'chains' => [],
'source_evidence' => app(InvestmentTerritorialTraceability::class)->sources($dossier->project_id ? 'projects' : 'economic_clusters', $dossier->project_id ?? $dossier->cluster_id ?? '')];
if ($dossier->project_id) {
$result['locations'] = $this->projectLocations($dossier->project_id, $management);
$memberships = $this->memberships()->where('cp.project_id', $dossier->project_id)->get(['cp.cluster_id', 'cp.role', 'cp.validation_status', 'cp.source_reference']);
@@ -0,0 +1,159 @@
<?php
namespace App\Services;
use App\Http\Controllers\InvestmentPortfolioController;
use App\Models\Opportunity;
use Illuminate\Support\Facades\DB;
/** Attach evidence to existing relationships; never grant legal approval or infer new geography. */
class InvestmentTerritorialTraceability
{
public function directory(): string
{
return config('investment.traceability_directory', storage_path('app/private/investmoz-v2'));
}
public function fingerprint(): string
{
return hash('sha256', implode('', array_map(fn ($file) => hash_file('sha256', base_path($file)), [
'app/Services/InvestmentTerritorialTraceability.php', 'app/Http/Controllers/InvestmentRelationshipRepairController.php',
'resources/js/components/territorial-traceability-plan.tsx', 'tools/investmoz-v2-prepare-territorial-plan.cjs',
])));
}
/** @return array<string,mixed>|null */
public function preview(): ?array
{
$file = $this->directory().'/territory-activation-plan.json';
if (! is_file($file)) {
return null;
}
$bytes = file_get_contents($file);
abort_if($bytes === false, 503, 'Plano não pode ser lido.');
$plan = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
abort_unless(is_array($plan) && ($plan['schema'] ?? null) === 1 && is_array($plan['rows'] ?? null), 422, 'Plano inválido.');
$hash = hash('sha256', $bytes);
$last = DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->where('after_data->plan_hash', $hash)->orderByDesc('occurred_at')->orderByDesc('id')->first();
return ['hash' => $hash, 'plan' => $plan, 'code_matches' => ($plan['code_hash'] ?? '') === $this->fingerprint(),
'applied' => $last?->action === 'traceability_applied', 'backup_ready' => $this->backupReady($plan['target'], $hash)];
}
/** @param array<string,mixed> $target */
private function backupReady(array $target, string $hash): bool
{
$file = $this->directory().'/territory-backup-proof.json';
if (! is_file($file)) {
return false;
}
$proof = json_decode(file_get_contents($file) ?: '{}', true);
return ($proof['restore_verified'] ?? false) === true && ($proof['reversal_verified'] ?? false) === true
&& ($proof['plan_hash'] ?? '') === $hash && ($proof['code_hash'] ?? '') === $this->fingerprint()
&& ($proof['target'] ?? []) === $target && strtotime($proof['verified_at'] ?? '') >= time() - 86400
&& is_file($proof['dump_path'] ?? '') && hash_file('sha256', $proof['dump_path']) === ($proof['sha256'] ?? null);
}
/** @param array<string,mixed> $guard */
private function checkGuard(array $guard): void
{
abort_unless(in_array($guard['table'] ?? '', ['projects', 'economic_clusters', 'investment_zones', 'opportunities', 'investment_dossiers', 'cluster_projects', 'project_locations', 'investment_zone_locations', 'investment_legal_sources'], true), 422, 'Tabela fora do plano permitido.');
$row = DB::table($guard['table'])->where('id', $guard['id'])->lockForUpdate()->first();
abort_unless($row !== null, 409, 'Registo do plano já não existe.');
foreach ($guard['expected'] as $field => $value) {
abort_unless(property_exists($row, $field) && ($row->$field === null ? $value === null : $value !== null && (string) $row->$field === (string) $value), 409, 'Registo alterado desde a pré-visualização: '.$guard['table'].'.'.$field);
}
}
/** @return array<string,mixed> */
public function apply(string $hash, int $actor, bool $reverse = false): array
{
$preview = $this->preview();
abort_unless($preview && hash_equals($preview['hash'], $hash) && $preview['code_matches'], 409, 'Plano ou código alterado. Reveja a pré-visualização.');
$plan = $preview['plan'];
$identity = DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port');
abort_unless($identity->database === $plan['target']['database'] && (int) $identity->port === (int) $plan['target']['port'], 409, 'Destino diferente do plano.');
if (app()->environment('testing')) {
abort_unless(str_ends_with($identity->database, '_test') && (int) $identity->port === 55441, 409, 'QA exige base isolada.');
}
abort_unless($preview['backup_ready'], 422, 'Backup/restauro/reversão verificados e atualizados são necessários.');
return DB::transaction(function () use ($plan, $hash, $actor, $reverse) {
DB::select('SELECT pg_advisory_xact_lock(81026, 160000)');
$last = DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->where('after_data->plan_hash', $hash)->orderByDesc('occurred_at')->orderByDesc('id')->lockForUpdate()->first();
if ((! $reverse && $last?->action === 'traceability_applied') || ($reverse && $last?->action === 'traceability_reversed')) {
return ['replayed' => true, 'changed' => 0];
}
abort_if(! $reverse && $last?->action === 'traceability_reversed', 409, 'Plano revertido; prepare um novo plano com o estado atual.');
$before = [];
$applied = ['updates' => []];
if ($reverse) {
abort_unless($last?->action === 'traceability_applied', 409, 'Não há aplicação para reverter.');
$applied = json_decode($last->after_data, true);
foreach ($applied['updates'] as $update) {
$this->checkGuard(['table' => 'cluster_projects', 'id' => $update['id'], 'expected' => $update['after']]);
$values = $update['before'];
$values['version_no'] = $update['after']['version_no'] + 1;
DB::table('cluster_projects')->where('id', $update['id'])->update($values);
}
} else {
foreach ($plan['guards'] as $guard) {
$this->checkGuard($guard);
}
foreach ($plan['updates'] as $update) {
abort_unless($update['table'] === 'cluster_projects' && array_diff(array_keys($update['values']), ['source_reference', 'evidence_reference']) === [], 422, 'Alteração fora do âmbito da rastreabilidade.');
$row = DB::table('cluster_projects')->where('id', $update['id'])->first();
$after = [...$update['values'], 'version_no' => $row->version_no + 1];
$before[] = ['id' => $row->id, 'before' => ['source_reference' => $row->source_reference, 'evidence_reference' => $row->evidence_reference, 'version_no' => $row->version_no], 'after' => $after];
DB::table('cluster_projects')->where('id', $row->id)->update($after);
}
}
DB::table('audit_events')->insert(['source_system' => 'territory-traceability-v2', 'entity_type' => 'territorial_sources', 'actor_user_id' => $actor,
'action' => $reverse ? 'traceability_reversed' : 'traceability_applied', 'data_classification' => 'internal', 'occurred_at' => DB::raw('clock_timestamp()'),
'after_data' => json_encode(['plan_hash' => $hash, 'rows' => $plan['rows'], 'updates' => $before, 'no_new_geography' => true, 'official_policy' => 'accepted_without_revalidation'], JSON_THROW_ON_ERROR)]);
return ['replayed' => false, 'changed' => $reverse ? count($applied['updates']) : count($before), 'associations' => count($plan['rows'])];
});
}
/** @return list<array<string,mixed>> */
public function sources(string $kind, string|int $id): array
{
$visible = match ($kind) {
'projects' => InvestmentPortfolioController::visible()->where('project_id', $id)->exists(),
'economic_clusters' => InvestmentPortfolioController::visible()->where('cluster_id', $id)->exists(),
'investment_zones' => app(InvestmentZones::class)->visible()->whereKey($id)->exists(),
'opportunities' => Opportunity::published()->whereKey($id)->exists(),
default => false,
};
if (! $visible) {
return [];
}
$events = DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->orderBy('occurred_at')->orderBy('id')->get(['action', 'after_data']);
$plans = [];
foreach ($events as $event) {
$data = json_decode($event->after_data, true);
$plans[$data['plan_hash']] = $event->action === 'traceability_applied' ? $data['rows'] : [];
}
$sources = [];
foreach ($plans as $rows) {
foreach ($rows as $row) {
if ($row['kind'] === $kind && (string) $row['record_id'] === (string) $id && ($row['public'] ?? false)) {
if (! empty($row['dossier_id']) && ! InvestmentPortfolioController::visible()->whereKey($row['dossier_id'])->exists()) {
continue;
}
if (! empty($row['public_location_id']) && ! app(InvestmentRelations::class)->locations([['id' => $row['public_location_id'], 'role' => $row['role']]])) {
continue;
}
if (! empty($row['association_id']) && ! app(InvestmentRelations::class)->memberships()->where('cp.id', $row['association_id'])->exists()) {
continue;
}
$sources[] = array_intersect_key($row, array_flip(['association', 'role', 'source_reference', 'source_quote', 'supports', 'traceability', 'official_source_accepted', 'existing_validation']));
}
}
}
return $sources;
}
}
@@ -0,0 +1,46 @@
# V2 — rastreabilidade territorial e aplicação autenticada
Entrega preparada em 9 de outubro de 2026 (Africa/Johannesburg). Complementa [a qualidade territorial por registo](qualidade-territorial.md) e [a reconciliação documental dos 46 registos](reconciliacao-fontes-territoriais.md).
## Aceitação e âmbito
A regra permanente de `AGENTS.md` mantém a aceitação das fontes oficiais/governamentais identificadas. Não foi repetida a verificação da proveniência. As 11 referências oficiais/legais, os 14 estados cadastrais `validated` e a aceitação existente do pacote INE foram reutilizados. Recuperar uma referência não desvalida os dados; os seis estados legais `pending` preexistentes não foram alterados por este incremento.
O plano abrange 46 registos: seis oportunidades, 16 projetos, oito clusters e 16 zonas. Contém **112 entradas de rastreabilidade**: proveniência do registo, implantação cadastrada, locais dos projetos, impacto, associações territoriais das zonas e membros dos clusters, preservando os papéis originais. Cada entrada identifica fonte, informação fundamentada, estado existente, pendência específica e efeito na interface. Citações já existentes nas 28 associações de projetos foram reutilizadas, sem nova validação.
Foram recuperadas citações explícitas para as **13 associações cluster–projeto existentes**. A proposta preenche apenas `source_reference` e `evidence_reference` em `cluster_projects`, incrementando a versão. Não muda `validation_status`, `association_type`, autoria original ou classificação de divulgação. O dossier de programação citado documenta essa relação existente; não recebe a classificação de documento oficial por esta operação.
As seis oportunidades têm território principal, mas não têm correspondência explícita a âncoras/clusters nas referências já reconciliadas. O plano não cria essas ligações por setor ou proximidade. Há **41 entradas de pendência**, incluindo essas seis correspondências e 35 pontes de rastreabilidade; não são 41 invalidações nem pedidos de revalidação da origem oficial. A correspondência técnica entre o PDF INE aceite e o workbook administrativo não foi presumida.
## Pré-visualização e efeitos
Página existente: **http://127.0.0.1:8000/management/relationship-repair**, com autenticação e a permissão de gestão já utilizada pelo projeto. O bloco inicial **“V2 — aplicar rastreabilidade territorial”** apresenta o destino `investemoz:5432`, os campos atuais e propostos das 13 alterações, a fonte/citação e as 112 entradas por associação. O plano anterior da página permanece preservado e separado.
A ação desta entrega é exclusivamente **“Aplicar rastreabilidade territorial V2”**. A operação é transacional, verifica o hash do plano/código, o destino, o backup e o estado atual de cada registo. Conflitos apresentam um erro no próprio bloco e não aplicam parte do plano. O botão fica desativado durante a operação. Pedidos repetidos do mesmo plano aplicado não criam duplicados.
O evento de auditoria guarda a projeção das fontes, o ator autenticado e os valores antes/depois. As fichas autorizadas mostram o fundamento por associação; as permissões atuais dos dossiers, locais e membros continuam a ser verificadas. Pesquisa, filtros e mapa reutilizam as relações existentes; preencher referências não altera implantação, contexto, impacto, abrangência, geometria ou disponibilidade. Registos sem coordenadas continuam consultáveis.
## Backup, compatibilidade e reversão
Backup atualizado restaurado em **`desk_restore_20261008_235546_test:55441`**. Foram comparadas **288 tabelas por contagem e hash dos dados**, e restaurados/verificados quatro ficheiros documentais mais a configuração privada. Isto comprova restauro; não declara scan antimalware desses documentos.
Na cópia restaurada, o mesmo plano aplicou 13 alterações, repetiu a operação sem duplicados e reverteu as 13. Valores de referência e estados de validação foram restituídos, com versões monotónicas e histórico de auditoria preservado. Onze tabelas territoriais/documentais relacionadas mantiveram os mesmos hashes; não houve nova geografia nem novas ligações de oportunidades.
A prova está vinculada ao plano e ao código, com validade de 24 horas: **até 10/10/2026 às 01:55:55, Africa/Johannesburg**. Após expiração ou mudança do código/plano, o botão exige nova preparação. Não há migrations nesta aplicação.
Após aplicação, a mesma página disponibiliza **“Reverter este incremento de rastreabilidade”**. A reversão restitui apenas os campos deste incremento, preservando eventos e versões. Se os campos/versões já tiverem sido alterados depois da aplicação, recusa sobrescrever a alteração posterior. Recuperação integral do backup permanece um procedimento operacional separado; nunca executar restauro de QA contra a base ativa.
## Evidência e estado
- 56 testes PHP passaram, com 884 asserções: relações, território, zonas, carteira, unificação e o novo fluxo autenticado. Os novos testes cobrem permissões, idempotência, reversão, consulta autorizada, conflito e prova expirada/alterada.
- Quatro testes de reconciliação verificam os estados independentes e a preservação da aceitação. São testes de comportamento com fixtures; não reabrem a proveniência dos documentos.
- TypeScript, lint/formatação, PHPStan dos serviços/controladores afetados e compilação passaram.
- Navegador QA: pré-visualização autenticada, clique no botão V2, HTTP 302, sucesso visível, recuperação após recarregar, fonte por associação na ficha e reversão; sem erros de JavaScript. Somente login e duas ações de gestão na cópia isolada.
- Dez casos territoriais de navegador passaram em computador/móvel, sem escritas: localização própria, contexto associado, vários locais, informação insuficiente e localização sem coordenadas.
- Leitura ativa em `READ ONLY` às 00:00:12 UTC de 9 de outubro: domínio territorial inalterado e **zero eventos de aplicação deste incremento**. O hash comparado consta da prova privada; sucesso em QA não é aplicação ativa.
Provas privadas em `storage/app/private/investmoz-v2`: `territory-activation-plan.json`, `territory-backup-proof.json`, `territory-reversal-evidence.json`, `territory-activation-browser.json` e `territory-activation-active-preservation.json`. Backup, configuração e matrizes privadas não entram no commit nem são publicados.
**Preparado:** código, pré-visualização e aplicação autenticada. **Validado em QA:** aplicação, reversão, permissões e apresentação. **Aplicado na base ativa:** nenhuma alteração de dados deste incremento; ação autenticada do responsável permanece por executar.
Continuam separadas: questões jurídicas específicas de âmbito/versão/aplicabilidade ainda não resolvidas; diferença de cobertura do inventário; ClamD real; primeiro pedido real no Balcão. A aceitação da proveniência oficial não foi reaberta e nenhuma dessas pendências foi concluída pelos testes territoriais.
+39
View File
@@ -124,3 +124,42 @@ V2-11/V2-15: DOCX lido normalmente; novo hash `d0c06a759693e7b74c6daec14539e247e
A próxima frente deve reutilizar as entidades e relações atuais, identificar vínculos ausentes ou contraditórios e distinguir localização indicada de disponibilidade/capacidade comprovada. Correções e associações devem ser demonstradas em QA; qualquer mudança de dados ativos continua dependente de backup/restauro e ação autenticada prevista no projeto.
Verificação deste incremento: 19 testes PHP/514 asserções de biblioteca e compatibilidade, 28 testes de regressão do simulador, TypeScript, formatação/lint e percurso de navegador nas três páginas em computador/móvel. A releitura repetiu os controlos afetados das 14 etapas e referências. **Nenhuma escrita na base ativa.** ClamD real, validação das fontes institucionais e primeiro pedido real no Balcão continuam três pendências separadas; estes testes não as concluem.
## Entrega territorial — relações e apresentação em QA
V2-08/V2-09: [resultados por registo, fontes, motivos e propostas](qualidade-territorial.md). Leitura `READ ONLY` da base ativa: seis oportunidades, 16 projetos, oito clusters e 16 zonas; 1 921 nós percorridos sem inconsistência estrutural detetada e zero geometrias cadastradas. Isto não certifica geografia oficial. As seis oportunidades têm território principal, sem vínculos cadastrados a projetos/clusters; não foram criadas associações por proximidade.
| Âmbito | Resultado | Aplicado à base ativa |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| Código — fichas e pesquisa | Implantação declarada distinta de locais/impacto de projetos e abrangência de clusters; fontes/datas ausentes explícitas; filtros de implantação/contexto e pesquisa territorial | Código local, demonstrado em QA; nenhuma alteração de dados/esquema |
| Código — mapa e navegação | Vários locais preservados com seleção independente; polígonos cadastrados com proveniência; dossier/oportunidade nos dois sentidos e território/pesquisa | Sem novos pontos ou polígonos ativos |
| Propostas de dados | 46 registos discriminados, mantendo relações e papéis existentes; lacunas de fonte/data e vínculos ausentes para revisão documental | Não aplicadas; backup/restauro e ação autenticada continuam necessários |
| QA | 53 testes PHP/859 asserções; repetição dos 12 afetados/186 asserções; 10 casos de navegador em computador/móvel, TypeScript, PHPStan dos serviços, lint e build | Exclusivamente `investmoz_v2_20261008_test:55441`; sem escritas de navegador |
| Pendências documentais | Evidência da implantação, fontes/datas das relações, limites oficiais, disponibilidade/capacidade e vínculos oportunidade/projeto/cluster | Nenhuma confirmação institucional inferida |
Continuam separadas: **diferença de cobertura do inventário**, **validação jurídica e das fontes institucionais**, **ClamD real** e **primeiro pedido real no Balcão**. O sucesso destes testes não conclui nenhuma delas.
Verificação final: a repetição do navegador detetou uma animação do mapa após remoção (`_leaflet_pos`); ciclo de vida e limpeza de pontos/áreas corrigidos, dez casos finais passaram sem erros. Duas leituras `READ ONLY` confirmaram o mesmo hash para dados/relações do domínio territorial auditado antes e depois da QA (`territory-active-preservation.json`). Nenhuma proposta de dados foi aplicada.
## Retificação da classificação das fontes territoriais
**Decisão expressa do responsável — 08/10/2026:** dados e documentos de origem oficial/governamental identificada, incluindo BR, decretos e leis, são aceites como fontes fidedignas e válidas, sem segunda ou terceira validação da sua autenticidade/proveniência e sem confirmação por outra fonte. Regra permanente registada em `AGENTS.md`. Pendências históricas genéricas de revalidação dessa proveniência ficam substituídas por esta decisão; lacunas de ligação ao exemplar são rastreabilidade, não invalidade. Âmbito, versão/período aplicável e parâmetros de cálculo continuam a ser extraídos da própria fonte oficial aceite, sem transformar consulta/publicação em ativação de regras.
Critério confirmado pelo responsável: **documentos oficiais são a única fonte de validação oficial**. A origem oficial declarada dos dados em uso é preservada; ausência de fonte/data numa coluna não invalida os dados nem exige repetir uma validação já documentada. [Reconciliação dos 46 registos](reconciliacao-fontes-territoriais.md): 11 referências oficiais/legais identificadas, 14 estados `validated` de zonas preservados, seis estados legais `pending` preexistentes apresentados separadamente e aceitação do pacote INE mantida, com hash do exemplar conferido. A correspondência técnica INE/workbook não foi inventada.
Propostas e [relatório territorial](qualidade-territorial.md) corrigidos para distinguir validação existente, rastreabilidade por recuperar e questões jurídicas efetivamente pendentes. Programação e promotores não substituem documentos oficiais. Quatro testes de reconciliação com fixtures sintéticos passaram, verificando preservação de validações, ausência de revogação por campo vazio, separação dos estados legais e recusa de transformar promotor em fonte oficial. Nenhuma escrita ativa ou ativação de cálculo; as quatro pendências anteriores mantêm âmbitos separados.
## Incremento territorial — 9 de outubro de 2026
[Plano autenticado, restauro e reversão](ativacao-rastreabilidade-territorial.md). A aceitação das fontes oficiais identificadas foi reutilizada, sem nova verificação de proveniência.
| Requisito | Preparado / validado em QA | Aplicado na base ativa |
| --- | --- | --- |
| V2-08 — rastreabilidade | 46 registos reconciliados; 112 entradas por associação; 11 referências oficiais/legais, 14 fichas validadas e pacote INE preservados | Nenhuma nova gravação; faltas de ligação documental não revogam aceitação |
| V2-08 — relações | 13 citações cluster–projeto recuperadas; proposta de preencher referências existentes; 28 vínculos territoriais de projetos reutilizados | Proposta não aplicada; sem novas relações inferidas |
| V2-08/V2-09 — fichas, filtros e mapa | Papéis e geometria preservados; fundamento por associação nas fichas autorizadas; navegação e filtros da entrega anterior reutilizados | Código local; sem novos pontos, limites ou capacidade atribuída |
| V2-13 — aplicação | Página `/management/relationship-repair`, botão específico V2, conflitos visíveis, idempotência e reversão auditada | Ação autenticada ainda por executar |
| V2-13 — backup/restauro | 288 tabelas comparadas e cinco ficheiros restaurados; aplicação/reversão ensaiadas em `desk_restore_20261008_235546_test:55441` | Leitura/backup apenas; prova vinculada ao plano/código, validade 24 horas |
| QA | 56 testes PHP/884 asserções, quatro testes de reconciliação, TypeScript, lint, PHPStan, build e percurso autenticado no navegador | Zero eventos deste incremento na leitura ativa final; dados territoriais inalterados |
Restam pontes de rastreabilidade e seis correspondências oportunidade–âncora/cluster sem fundamento explícito recuperado; não se retirou a aceitação existente. Questões jurídicas específicas, diferença de cobertura do inventário, ClamD real e primeiro pedido real permanecem separadas.
@@ -0,0 +1,96 @@
# Qualidade territorial e relações — entrega V2
## Retificação — validação oficial e rastreabilidade
**Só os documentos oficiais fundamentam validação oficial**, conforme o critério do responsável. A declaração de que os dados em uso foram extraídos desses documentos é preservada. A revisão anterior confundiu, em várias propostas, ausência de campos de fonte/data com necessidade de voltar a validar o dado. Essas propostas foram corrigidas: recuperar a ponte documental não desvalida nem revoga o dado.
A [matriz por registo](reconciliacao-fontes-territoriais.md) recupera 11 referências oficiais/legais identificadas, preserva as 14 fichas de zonas com estado `validated` e distingue os seis estados `pending` que **já existiam nas fontes legais**. Também conserva a aceitação explícita do exemplar INE, com hash conferido. Nenhuma nova validação, revogação, alteração ativa ou ativação de cálculo foi efetuada. Referências de programação/promotores continuam identificadas como tais, sem substituir o documento oficial.
Os restantes registos têm citação e/ou ligação incompleta ao exemplar oficial no cadastro revisto. O resultado é **rastreabilidade por recuperar**, não «dado inválido» nem «fonte oficial inexistente». A matriz separa a validação do cadastro, a validação da fonte legal, a proveniência documental e o âmbito da evidência.
Leitura protegida em 2026-10-08T17:11:53+00:00, base ativa investemoz:5432, transação READ ONLY terminada por ROLLBACK. Nenhum dado ativo alterado. Evidência completa privada: `territory-audit.json`.
## Resultados por registo e propostas de dados
Cada linha identifica o estado cadastrado, a fonte consultada, motivo e impacto da proposta. A leitura de um cadastro ou dossier não valida a fonte institucional. Sem associação explícita não se cria uma relação por nome semelhante, cluster ou proximidade. Não foram consultados sites institucionais nesta auditoria.
| Registo | Relações/localizações cadastradas | Fonte consultada | Proposta, motivo e impacto |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| INV-MZ-AGR-001 | principal cadastrada: Moçambique → Nampula; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | Cadastro da oportunidade; fonte da relação e data ausentes | Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| INV-MZ-ENE-002 | principal cadastrada: Moçambique → Zambezia; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | Cadastro da oportunidade; fonte da relação e data ausentes | Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| INV-MZ-TUR-003 | principal cadastrada: Moçambique → Inhambane; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | Cadastro da oportunidade; fonte da relação e data ausentes | Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| INV-MZ-INF-004 | principal cadastrada: Moçambique → Sofala; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | Cadastro da oportunidade; fonte da relação e data ausentes | Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| INV-MZ-BLUE-005 | principal cadastrada: Moçambique → Zambezia; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | Cadastro da oportunidade; fonte da relação e data ausentes | Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| INV-MZ-FIN-006 | principal cadastrada: Moçambique → Maputo Cidade; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | Cadastro da oportunidade; fonte da relação e data ausentes | Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A03 | secondary: Moçambique → Cabo Delgado; secondary: Moçambique → Nampula; secondary: Moçambique → Niassa; secondary: Moçambique → Tete; impact: Moçambique → Zambezia; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A08 | primary: Moçambique → Zambezia; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A10 | primary: Moçambique → Tete; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A04 | primary: Moçambique → Nampula; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A07 | secondary: Moçambique → Tete; secondary: Moçambique → Manica; primary: Moçambique → Sofala; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A06 | secondary: Moçambique → Tete; secondary: Moçambique → Zambezia; secondary: Moçambique → Manica; secondary: Moçambique → Sofala; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A02 | Sem localização estruturada; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A12 | primary: Moçambique → Zambezia; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| EXAMPLE-PROJECT-001 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | example | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A14 | primary: Moçambique → Gaza; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A01 | Sem localização estruturada; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A13 | primary: Moçambique → Inhambane; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A15 | impact: Moçambique → Maputo Província → MOAMBA; primary: Moçambique → Maputo Província; impact: Moçambique → Maputo Província → BOANE; impact: Moçambique → Maputo Província → NAMAACHA; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A11 | secondary: Moçambique → Tete; secondary: Moçambique → Zambezia; secondary: Moçambique → Zambezia → NICOADALA; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A05 | secondary: Moçambique → Cabo Delgado; secondary: Moçambique → Nampula; projetos: 0; clusters: 1; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| A09 | primary: Moçambique → Zambezia; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-05 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 1; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-06 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-01 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 3; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-04 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 2; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-08 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-03 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 3; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-07 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| CLU-02 | Sem localização estruturada; projetos: 0; clusters: 0; membros: 4; ligações de zona: 0 | upgrade-v4; upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto; Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-TOPUITO-PARK | administrative: Moçambique → Nampula; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://mozparks.co.mz/construction-begins-on-sme-village-at-topuito-industrial-park/ — Construction begins on SME Village at Topuito Industrial Park | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-CDP-ANCUABE | administrative: Moçambique → Cabo Delgado → ANCUABE; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ — MozParks — Cabo Delgado parks initiative | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-BELULUANE-PARK | administrative: Moçambique → Maputo Província → BOANE → MATOLA RIO; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-TOPUITO-ZFI | administrative: Moçambique → Nampula; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-s%C3%A9rie-p%C3%A1g-2349 — Decreto n.º 57/2023 — criação da ZFI de Topuito | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-MANGA-MUNGASSA-ZEE | administrative: Moçambique → Sofala → CIDADE DA BEIRA; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/ — Decreto n.º 22/2012, de 6 de Junho | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-CDP-PALMA | administrative: Moçambique → Cabo Delgado → PALMA; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ — MozParks — Cabo Delgado parks initiative | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-MOCUBA-ZFI | administrative: Moçambique → Zambezia → MOCUBA → CIDADE DE MOCUBA; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/zif/mocuba-free-industrial-zone/ — Decreto n.º 29/2014, de 6 de Junho | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-CRUSSE-JAMALI-ZEE | legal_scope: Moçambique → Nampula → MOSSURIL → MATIBANE; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-70-de-300813-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-571 — Decreto n.º 47/2013, de 30 de Agosto | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-MOCUBA-ZEE | legal_scope: Moçambique → Zambezia → MOCUBA; legal_scope: Moçambique → Zambezia → LUGELA → MUNHAMADE; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-46-de-060614-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1259 — Decreto n.º 28/2014, de 6 de Junho | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-CDP-MONTEPUEZ | administrative: Moçambique → Cabo Delgado → MONTEPUEZ; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ — MozParks — Cabo Delgado parks initiative | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-BELULUANE-ZFI | administrative: Moçambique → Maputo Província → BOANE → MATOLA RIO; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-MUNHAVA-PARK | administrative: Moçambique → Sofala → CIDADE DA BEIRA; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://sdubeira.co.mz/project/parque-industrial-logistico/ — Parque Industrial e Logístico — SDUBeira | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-REVUBUE-ZFI | legal_scope: Moçambique → Tete → MOATIZE; legal_scope: Moçambique → Tete → CHIUTA; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/revubue-free-industrial-zone/ — Revúbuè Free Industrial Zone — APIEX | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-NACALA-ZEE | legal_scope: Moçambique → Nampula → NACALA-VELHA; legal_scope: Moçambique → Nampula → NACALA-PORTO; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/nacala-special-economic-zone/ — Decreto n.º 76/2007, de 18 de Dezembro | Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide; Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-MINHEWENE-ZFI | administrative: Moçambique → Nampula; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://apiex.gov.mz/zona-franca-industrial-de-minhewene/ — Zona Franca Industrial de Minhewene — APIEX | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
| MOZ-CDP-BALAMA | administrative: Moçambique → Cabo Delgado → BALAMA; projetos: 0; clusters: 0; membros: 0; ligações de zona: 0 | https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ — MozParks — Cabo Delgado parks initiative | Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade; Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável |
## Hierarquia e cartografia
1921 nós administrativos/locais percorridos; 0 caminhos com inconsistência estrutural identificada por esta verificação. Ausência de inconsistência cadastral não certifica fronteiras oficiais. 0 geometrias ativas cadastradas: nenhum ponto, polígono ou centroide gerado.
## Correções de código e QA
- Ficha apresenta implantação declarada separada dos locais/impacto dos projetos e abrangência dos clusters; fonte/data ausentes explícitas.
- Filtros distinguem implantação declarada de apenas contexto associado; localizações sem coordenadas continuam pesquisáveis.
- Mapa mantém vários locais do mesmo investimento e seleção independente; áreas cadastradas são apresentadas com proveniência, sem homologar limites.
- Corrigida a transição do mapa após seleção/navegação: animações e deslocação automática de popups deixam de correr após remoção; callbacks de redimensionamento respeitam o ciclo de vida. Ao mudar filtros, pontos e áreas anteriores são limpos durante a nova consulta. O ensaio final repetiu os dez casos sem o erro `_leaflet_pos` observado na repetição intermédia.
- Associação existente continua a permitir navegação oportunidade → dossier → oportunidade e território → pesquisa. Permissões e relações revogadas preservadas.
QA isolada em `investmoz_v2_20261008_test:55441`: **53 testes PHP, 859 asserções** (relações, território, zonas, carteira e unificação). Após o ajuste de tipagem do agrupamento, os 12 testes afetados passaram novamente, com 186 asserções. PHPStan dos dois serviços sem erros; TypeScript, formatação/lint e compilação concluídos. A compilação conserva os avisos de dependência opcional `fontaine` e tamanho de chunks, sem falha de geração.
Navegador: **10 casos**, cinco cenários em 1440/390 px — localização própria, apenas contexto associado, duas localizações, informação insuficiente e localização documental sem coordenadas. Verificados os destinos e o retorno pelo dossier, filtro de contexto, seleção independente dos marcadores, polígonos com fonte/data e consulta sem coordenadas. Sem erros JavaScript e sem pedidos de escrita; tráfego externo bloqueado. Fontes e geometrias destes fixtures são sintéticas, exclusivamente QA, e não comprovam dados reais.
Evidência privada: `territory-php-tests.txt`, `territory-php-affected.txt`, `territory-browser.json` e capturas `territory-browser-1440.png`/`territory-browser-390.png`. Auditoria e propostas completas por ID: `territory-audit.json` e `territory-proposals.json`. Os ficheiros privados não devem ser publicados.
Preservação ativa conferida por duas leituras `READ ONLY`, às 17:11:53 e 17:25:31 UTC: os registos, relações, nós, geometrias e dossiers do domínio auditado têm o mesmo hash de comparação `0a83af33d02ca15489a4e8db8a1d741dd39c3cd9c7660df95e33ee64432dd702`. Prova privada `territory-active-preservation.json`; primeira leitura conservada em `territory-audit-before-qa.json`. Esta comparação não é um backup/restauro nem autoriza futuras alterações ativas.
**Limitações de rastreabilidade e âmbito:** nenhuma geometria foi encontrada no cadastro; isto não prova ausência de mapas nos documentos oficiais. O pivot de localizações da oportunidade não conserva fonte/data: falta recuperar a ligação ao documento utilizado, sem invalidar a localização. As seis oportunidades não têm vínculos cadastrados com projetos/clusters; a falta de vínculo técnico não prova ausência de relação documental. Os vínculos de zonas existentes são reutilizados quando presentes e autorizados; não foram criados novos vínculos. Cada validação mantém o seu âmbito: um documento sobre localização não confirma automaticamente outros atributos. Propostas desta tabela não são alterações aplicadas nem revogação de validações.
## Pendências independentes
Diferença de cobertura do inventário documental, questões jurídicas específicas de âmbito/versão/aplicabilidade ainda não resolvidas, ClamD real e primeiro pedido real no Balcão permanecem separados. A aceitação das fontes oficiais identificadas não é reaberta. Nenhum teste territorial conclui essas pendências. Aplicação futura de dados exige backup/restauro confirmado e ação autenticada existente.
## Continuação — rastreabilidade e ativação, 9 de outubro
[Plano concreto de aplicação autenticada](ativacao-rastreabilidade-territorial.md): 112 entradas de fonte/fundamento para os mesmos 46 registos; 13 referências de relações cluster–projeto propostas, sem alterar validações ou criar ligações por proximidade. Os 28 vínculos territoriais existentes, as 11 referências oficiais/legais, as 14 fichas validadas e o pacote INE aceite foram reutilizados. Pendências de rastreabilidade preservam a aceitação existente.
Backup/restauro de 288 tabelas e cinco ficheiros verificado; aplicação e reversão testadas na cópia isolada. 56 testes PHP/884 asserções passaram e o navegador comprovou o clique V2, resposta HTTP, recuperação, fonte por associação na ficha e reversão. Nenhuma proposta deste incremento foi aplicada à base ativa. A pré-visualização autenticada mostra campos atuais/propostos, fonte, motivo e efeito antes da ação do responsável.
@@ -0,0 +1,76 @@
# Reconciliação das fontes e validações territoriais
## Decisão permanente sobre origem oficial
Em 08/10/2026, o responsável determinou que dados e documentos com origem oficial/governamental identificada — BR, decretos, leis e outros documentos oficiais — são aceites como fidedignos e válidos, sem segunda ou terceira validação da autenticidade/proveniência ou confirmação por outra fonte. Esta decisão substitui a exigência genérica de revalidar fontes oficiais nos relatórios anteriores. Os estados técnicos históricos abaixo são conservados como leitura do cadastro, não como obrigação de repetir essa validação.
Completar a referência ao exemplar, versão e citação é recuperar rastreabilidade. Identificar âmbito, período de aplicação ou alterações/revogações na própria documentação oficial é aplicar a fonte aceite; não questionar a sua credibilidade. Consulta autorizada, aplicação jurídica ao caso e utilização em cálculo mantêm funções distintas.
Critério do responsável: documentos oficiais são a única fonte de validação oficial. Programação, promotores, testes e leitura do cadastro não substituem esses documentos. Ausência de um campo de fonte/data é uma lacuna de rastreabilidade, não uma decisão de invalidade nem revogação.
Esta revisão conserva os estados existentes. Uma referência oficial identificada não significa que todas as afirmações da ficha estejam cobertas: o âmbito da citação deve ser conservado. Não foram concedidas novas validações nem ativados cálculos. A declaração do responsável sobre a origem oficial dos dados em uso é registada; as pontes técnicas ainda incompletas são identificadas abaixo, sem contradizer essa declaração.
## Evidência oficial e aceitação já existentes
INE — Codificador da Divisão Político-Administrativa: exemplar privado com SHA-256 `5c69b6f189a1d3f5307e0cf7dd8224554705158a682637d3105afad615db26e2`, coincidente com a aceitação de 2026-09-28. Decisão preservada: `declared_validated_by_owner` e `declared_authenticated_by_owner`, por instrução explícita do responsável. A aceitação não é reaberta. O próprio registo distingue-a da correspondência técnica: `unresolved_column_alignment`; zero territórios importados por essa decisão. Não se atribui automaticamente esse PDF aos 1 921 nós oriundos do workbook.
As seis fontes de diplomas existentes conservam `validation_status=pending` tal como já estava na base; esse estado não foi imposto por esta entrega. As 14 fichas de zonas com `validated` conservam esse estado, distinguindo validação do cadastro de validação jurídica do diploma associado. Referências de promotores não passam a fonte oficial por a ficha estar validada.
## Matriz por registo
| Registo | Validação já registada, preservada | Fonte/citação recuperada | Lacuna específica de rastreabilidade |
| ---------------------- | ------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| INV-MZ-AGR-001 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| INV-MZ-ENE-002 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| INV-MZ-TUR-003 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| INV-MZ-INF-004 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| INV-MZ-BLUE-005 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| INV-MZ-FIN-006 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| A03 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor de Nacala: Nampula, Cabo Delgado, Niassa, Tete e Norte da Zambézia, incluindo Nacala, Nampula, Cuamba, Palma e áreas ligadas ao corredor.; dossier 39f5973c-f4cd-49d0-aeea-1c0c71783550; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor de Nacala: Nampula, Cabo Delgado, Niassa, Tete e Norte da Zambézia, incluindo Nacala, Nampula, Cuamba, Palma e áreas ligadas ao corredor.; dossier 39f5973c-f4cd-49d0-aeea-1c0c71783550; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor de Nacala: Nampula, Cabo Delgado, Niassa, Tete e Norte da Zambézia, incluindo Nacala, Nampula, Cuamba, Palma e áreas ligadas ao corredor.; dossier 39f5973c-f4cd-49d0-aeea-1c0c71783550; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor de Nacala: Nampula, Cabo Delgado, Niassa, Tete e Norte da Zambézia, incluindo Nacala, Nampula, Cuamba, Palma e áreas ligadas ao corredor.; dossier 39f5973c-f4cd-49d0-aeea-1c0c71783550; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor de Nacala: Nampula, Cabo Delgado, Niassa, Tete e Norte da Zambézia, incluindo Nacala, Nampula, Cuamba, Palma e áreas ligadas ao corredor.; dossier 39f5973c-f4cd-49d0-aeea-1c0c71783550; auditoria não identificada; Dossier 39f5973c-f4cd-49d0-aeea-1c0c71783550, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A08 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Mocuba e Munhamade, província da Zambézia, com ligações a Quelimane, Nacala, Caia e Milange.; dossier 81d775e5-5dff-4fa2-98b1-72affb0a1082; auditoria não identificada; Dossier 81d775e5-5dff-4fa2-98b1-72affb0a1082, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A10 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Rio Zambeze, província de Tete, a jusante da Hidroeléctrica de Cahora Bassa; área exacta sujeita à documentação oficial do projecto.; dossier cad01a02-6072-4a11-823a-1ae379bc65da; auditoria não identificada; Dossier cad01a02-6072-4a11-823a-1ae379bc65da, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A04 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Crusse Jamali, zona de Nacala, província de Nampula.; dossier 0dccdd26-9796-4ef7-947b-5a2970ace379; auditoria não identificada; Dossier 0dccdd26-9796-4ef7-947b-5a2970ace379, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A07 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Região Centro, com incidência principal em Sofala e articulação produtiva com Manica e Tete, junto ao Corredor da Beira.; dossier 22323abb-91d9-43ed-98f3-1194a5250c9e; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Região Centro, com incidência principal em Sofala e articulação produtiva com Manica e Tete, junto ao Corredor da Beira.; dossier 22323abb-91d9-43ed-98f3-1194a5250c9e; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Região Centro, com incidência principal em Sofala e articulação produtiva com Manica e Tete, junto ao Corredor da Beira.; dossier 22323abb-91d9-43ed-98f3-1194a5250c9e; auditoria não identificada; Dossier 22323abb-91d9-43ed-98f3-1194a5250c9e, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A06 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Províncias de Tete, Manica, Zambézia e Sofala, articuladas pelos corredores da Beira e de Nacala e pelas bacias de irrigação identificadas.; dossier 15474e6a-90a3-4db4-b456-eb1c1c66227c; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Províncias de Tete, Manica, Zambézia e Sofala, articuladas pelos corredores da Beira e de Nacala e pelas bacias de irrigação identificadas.; dossier 15474e6a-90a3-4db4-b456-eb1c1c66227c; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Províncias de Tete, Manica, Zambézia e Sofala, articuladas pelos corredores da Beira e de Nacala e pelas bacias de irrigação identificadas.; dossier 15474e6a-90a3-4db4-b456-eb1c1c66227c; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Províncias de Tete, Manica, Zambézia e Sofala, articuladas pelos corredores da Beira e de Nacala e pelas bacias de irrigação identificadas.; dossier 15474e6a-90a3-4db4-b456-eb1c1c66227c; auditoria não identificada; Dossier 15474e6a-90a3-4db4-b456-eb1c1c66227c, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A02 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier 1125f069-61e2-4071-99f3-dd103caf3156, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A12 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Província da Zambézia, com incidência em zonas rurais, no rio Licungo, na Circular de Mocuba e em comunidades e ecossistemas costeiros.; dossier a023d6b2-1fa2-4fe7-9709-f93ae1f10e99; auditoria não identificada; Dossier a023d6b2-1fa2-4fe7-9709-f93ae1f10e99, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| EXAMPLE-PROJECT-001 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Sem ponte documental no cadastro revisto | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| A14 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Vale do Limpopo, província de Gaza: Chókwè, Xai-Xai, Guijá, Chongoene, Chibuto, Limpopo e Bilene.; dossier 6d973bb4-ab8a-49bc-92ed-286ed5b96fc4; auditoria não identificada; Dossier 6d973bb4-ab8a-49bc-92ed-286ed5b96fc4, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A01 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier 219e1dbb-939c-4ef0-92b7-7f78f8561fa0, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A13 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor turístico Vilanculos–Inhassoro–Bazaruto, província de Inhambane, incluindo áreas costeiras e ilhas associadas.; dossier bb15487f-1617-49e6-86b7-d617812b658b; auditoria não identificada; Dossier bb15487f-1617-49e6-86b7-d617812b658b, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A15 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Bacia Hidrográfica do Rio Maputo, província de Maputo, com incidência nos distritos de Boane, Moamba e Namaacha e ligação às bacias do Maputo e Incomáti.; dossier 1468b8d2-1b08-4c84-8000-ece7662edbf6; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Bacia Hidrográfica do Rio Maputo, província de Maputo, com incidência nos distritos de Boane, Moamba e Namaacha e ligação às bacias do Maputo e Incomáti.; dossier 1468b8d2-1b08-4c84-8000-ece7662edbf6; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Bacia Hidrográfica do Rio Maputo, província de Maputo, com incidência nos distritos de Boane, Moamba e Namaacha e ligação às bacias do Maputo e Incomáti.; dossier 1468b8d2-1b08-4c84-8000-ece7662edbf6; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Bacia Hidrográfica do Rio Maputo, província de Maputo, com incidência nos distritos de Boane, Moamba e Namaacha e ligação às bacias do Maputo e Incomáti.; dossier 1468b8d2-1b08-4c84-8000-ece7662edbf6; auditoria não identificada; Dossier 1468b8d2-1b08-4c84-8000-ece7662edbf6, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A11 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor entre Moatize, na província de Tete, e Macuse, no distrito de Nicoadala, província da Zambézia, incluindo Mutarara, Mopeia e Quelimane.; dossier 72c13e83-e8dd-465d-99a9-eb0ccaa4fd0b; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor entre Moatize, na província de Tete, e Macuse, no distrito de Nicoadala, província da Zambézia, incluindo Mutarara, Mopeia e Quelimane.; dossier 72c13e83-e8dd-465d-99a9-eb0ccaa4fd0b; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Corredor entre Moatize, na província de Tete, e Macuse, no distrito de Nicoadala, província da Zambézia, incluindo Mutarara, Mopeia e Quelimane.; dossier 72c13e83-e8dd-465d-99a9-eb0ccaa4fd0b; auditoria não identificada; Dossier 72c13e83-e8dd-465d-99a9-eb0ccaa4fd0b, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A05 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Eixo Namialo–Metoro, entre as províncias de Nampula e Cabo Delgado, com ligações a Pemba e Palma.; dossier 361789ad-1e14-4b64-8f1f-fad053886d5a; auditoria não identificada; upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Eixo Namialo–Metoro, entre as províncias de Nampula e Cabo Delgado, com ligações a Pemba e Palma.; dossier 361789ad-1e14-4b64-8f1f-fad053886d5a; auditoria não identificada; Dossier 361789ad-1e14-4b64-8f1f-fad053886d5a, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| A09 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6; citação: Cidade de Mocuba, província da Zambézia.; dossier 67263220-6849-468f-82f4-d5e1f5617190; auditoria não identificada; Dossier 67263220-6849-468f-82f4-d5e1f5617190, versão 1: upgrade do portal de investimentos v4 programacao.docx — 6.1 / 6.6 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-05 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier b9b1dc01-dca1-42d8-a9aa-40148dccfaf7, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-06 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier 0f29da56-b74a-4a69-afaa-b9a0d349580c, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-01 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier ca6a1427-0e2b-406e-94bd-da23630d5ec4, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-04 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier 5f0ff662-dc74-41de-adcd-de784a595321, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-08 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier ed2ea4e4-fca7-4cc8-bb8c-f99a1dfda73e, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-03 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier ec9494a2-22cc-423e-8a9e-efa1e05ac2b4, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-07 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier 523d2aa1-4c7d-4a84-97fc-220f4e845fd7, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| CLU-02 | Cadastro: campo de validação não existente/não registado nesta entidade; fonte legal: sem fonte legal ligada | Dossier 3cd11616-9099-477c-bf3e-09cddca74053, versão 1: upgrade do portal de investimentos v4 programacao.docx — 4.5.2 | Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada. |
| MOZ-TOPUITO-PARK | Cadastro: validated; fonte legal: sem fonte legal ligada | Referência promoter, não oficial: Construction begins on SME Village at Topuito Industrial Park; https://mozparks.co.mz/construction-begins-on-sme-village-at-topuito-industrial-park/ | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| MOZ-CDP-ANCUABE | Cadastro: pending; fonte legal: sem fonte legal ligada | Referência promoter, não oficial: MozParks — Cabo Delgado parks initiative; https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| MOZ-BELULUANE-PARK | Cadastro: validated; fonte legal: sem fonte legal ligada | Beluluane Industrial Park – Free Zone; https://apiex.gov.mz/beluluane-industrial-park-free-zone/; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-TOPUITO-ZFI | Cadastro: validated; fonte legal: sem fonte legal ligada | Decreto n.º 57/2023 — criação da ZFI de Topuito; https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-s%C3%A9rie-p%C3%A1g-2349; âmbito: Legal creation and reported legal area; primary review remains required. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-MANGA-MUNGASSA-ZEE | Cadastro: validated; fonte legal: pending | Decreto n.º 22/2012, de 6 de Junho; Manga-Mungassa Special Economic Zone – APIEX; Act of creation; article reference pending review; https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/; versão 1; Manga-Mungassa Special Economic Zone – APIEX; https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-CDP-PALMA | Cadastro: validated; fonte legal: sem fonte legal ligada | Referência promoter, não oficial: MozParks — Cabo Delgado parks initiative; https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| MOZ-MOCUBA-ZFI | Cadastro: validated; fonte legal: pending | Decreto n.º 29/2014, de 6 de Junho; Mocuba IFZ – APIEX; Act of creation; article reference pending review; https://apiex.gov.mz/zif/mocuba-free-industrial-zone/; versão 1; Mocuba IFZ – APIEX; https://apiex.gov.mz/zif/mocuba-free-industrial-zone/; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-CRUSSE-JAMALI-ZEE | Cadastro: validated; fonte legal: pending | Decreto n.º 47/2013, de 30 de Agosto; Conselho de Ministros — BR n.º 70 de 30.08.13; Act of creation; article reference pending review; https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-70-de-300813-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-571; versão 1; Conselho de Ministros — BR n.º 70 de 30.08.13; https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-70-de-300813-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-571; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-MOCUBA-ZEE | Cadastro: validated; fonte legal: pending | Decreto n.º 28/2014, de 6 de Junho; BR n.º 46 de 06.06.14 — Decreto n.º 28/2014; Act of creation; article reference pending review; https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-46-de-060614-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1259; versão 1; BR n.º 46 de 06.06.14 — Decreto n.º 28/2014; https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-46-de-060614-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1259; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-CDP-MONTEPUEZ | Cadastro: validated; fonte legal: sem fonte legal ligada | Referência promoter, não oficial: MozParks — Cabo Delgado parks initiative; https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
| MOZ-BELULUANE-ZFI | Cadastro: validated; fonte legal: pending | Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro; Beluluane Industrial Park – Free Zone; Act of creation; article and current legal effect pending review; https://apiex.gov.mz/beluluane-industrial-park-free-zone/; versão 1; Beluluane Industrial Park – Free Zone; https://apiex.gov.mz/beluluane-industrial-park-free-zone/; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-MUNHAVA-PARK | Cadastro: pending; fonte legal: pending | Decreto n.º 22/2012, de 6 de Junho; Manga-Mungassa Special Economic Zone – APIEX; Act of creation; article reference pending review; https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/; versão 1; Referência promoter, não oficial: Parque Industrial e Logístico — SDUBeira; https://sdubeira.co.mz/project/parque-industrial-logistico/ | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-REVUBUE-ZFI | Cadastro: validated; fonte legal: sem fonte legal ligada | Revúbuè Free Industrial Zone — APIEX; https://apiex.gov.mz/revubue-free-industrial-zone/; âmbito: APIEX-reported identity, district scope and area; primary legal text pending. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-NACALA-ZEE | Cadastro: validated; fonte legal: pending | Decreto n.º 76/2007, de 18 de Dezembro; Nacala Special Economic Zone – APIEX; Act of creation; article reference pending review; https://apiex.gov.mz/nacala-special-economic-zone/; versão 1; Nacala Special Economic Zone – APIEX; https://apiex.gov.mz/nacala-special-economic-zone/; âmbito: Referência institucional/promocional; não comprova regime jurídico, incentivos ou disponibilidade. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-MINHEWENE-ZFI | Cadastro: validated; fonte legal: sem fonte legal ligada | Zona Franca Industrial de Minhewene — APIEX; https://apiex.gov.mz/zona-franca-industrial-de-minhewene/; âmbito: APIEX-reported identity, province and area; primary legal text and site precision pending. | Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte. |
| MOZ-CDP-BALAMA | Cadastro: validated; fonte legal: sem fonte legal ligada | Referência promoter, não oficial: MozParks — Cabo Delgado parks initiative; https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/ | Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial. |
## Estados independentes e limites
Consulta autorizada permanece independente da completude da referência. Validação oficial deriva apenas da evidência documental oficial no âmbito aceite; o estado cadastral é preservado e apresentado separadamente. Utilização em regras/cálculos permanece inalterada. Não se transforma a data de criação, data de aplicação ou início de vigência em data da fonte.
O rótulo histórico de programação v4 foi preservado; a revisão anterior encontrou programação v5 corroborante, sem o exemplar v4. Esta diferença de rótulo é uma questão de proveniência, não uma revogação das 28 relações aplicadas. Programação não substitui o documento oficial na validação oficial.
Nenhuma base ativa foi escrita. A matriz integral privada está em `territory-source-reconciliation.json`. Diferença de cobertura do inventário, questões jurídicas realmente não resolvidas, ClamD real e primeiro pedido real continuam separados.
@@ -1,5 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import type { Map as LeafletMap, Marker } from 'leaflet';
import type { GeoJsonObject } from 'geojson';
import 'leaflet/dist/leaflet.css';
export type InvestmentMapFeature = {
@@ -13,13 +14,25 @@ export type InvestmentMapFeature = {
clusters?: string[];
corridors?: string[];
projects?: string[];
source_reference?: string | null;
source_date?: string | null;
};
};
export type InvestmentMapArea = {
geometry: GeoJsonObject;
title: string;
source_reference?: string | null;
source_date?: string | null;
url: string;
};
const noAreas: InvestmentMapArea[] = [];
export default function InteractiveInvestmentMap({
points,
areas = noAreas,
}: {
points: InvestmentMapFeature[] | null;
areas?: InvestmentMapArea[];
}) {
const element = useRef<HTMLDivElement>(null);
const map = useRef<LeafletMap | null>(null);
@@ -35,6 +48,9 @@ export default function InteractiveInvestmentMap({
const instance = L.map(element.current, {
scrollWheelZoom: false,
keyboard: true,
zoomAnimation: false,
fadeAnimation: false,
markerZoomAnimation: false,
}).setView([-18.5, 35], 5);
map.current = instance;
L.tileLayer(
@@ -55,6 +71,19 @@ export default function InteractiveInvestmentMap({
.addTo(instance);
L.control.scale({ imperial: false }).addTo(instance);
const bounds: [number, number][] = [];
for (const area of areas) {
const label = document.createElement('p');
label.textContent = `${area.title} · Polígono cadastrado, limites oficiais por verificar · Fonte: ${area.source_reference ?? 'Não indicada'} · Data: ${area.source_date ?? 'Não documentada'}`;
const layer = L.geoJSON(area.geometry)
.bindPopup(label)
.addTo(instance);
const extent = layer.getBounds();
if (extent.isValid())
bounds.push(
[extent.getSouth(), extent.getWest()],
[extent.getNorth(), extent.getEast()],
);
}
for (const [index, point] of (points ?? []).entries()) {
const [longitude, latitude] = point.geometry.coordinates;
if (
@@ -72,6 +101,7 @@ export default function InteractiveInvestmentMap({
for (const text of [
p.category,
p.location,
`Fonte: ${p.source_reference ?? 'Não indicada'} · Data: ${p.source_date ?? 'Não documentada'}`,
`Corredor: ${p.corridors?.join(', ') || 'Não associado'}`,
`Cluster: ${p.clusters?.join(', ') || 'Não associado'}`,
`Projecto âncora: ${p.projects?.join(', ') || 'Não associado'}`,
@@ -98,18 +128,22 @@ export default function InteractiveInvestmentMap({
alt: p.title,
keyboard: true,
})
.bindPopup(popup)
.on('click', () => setSelected(p.id))
.bindPopup(popup, { autoPan: false })
.on('click', () => setSelected(index))
.addTo(instance);
markers.current.set(p.id, marker);
markers.current.set(index, marker);
bounds.push([latitude, longitude]);
}
if (bounds.length)
instance.fitBounds(bounds, {
padding: [40, 40],
maxZoom: 14,
animate: false,
});
observer = new ResizeObserver(() => instance.invalidateSize());
observer = new ResizeObserver(() => {
if (!cancelled)
instance.invalidateSize({ animate: false, pan: false });
});
observer.observe(element.current);
})
.catch(() => {
@@ -122,7 +156,7 @@ export default function InteractiveInvestmentMap({
map.current = null;
markers.current.clear();
};
}, [points]);
}, [points, areas]);
return (
<section
className="space-y-3 rounded-xl border bg-white p-4 text-slate-900"
@@ -143,15 +177,15 @@ export default function InteractiveInvestmentMap({
<p role="status">A carregar localizações…</p>
) : (
<p role="status">
{points.length} investimentos com coordenadas disponíveis.
{points.length} locais com coordenadas disponíveis.
</p>
)}
<ul className="grid gap-2 md:grid-cols-2">
{(points ?? []).map((p) => (
{(points ?? []).map((p, index) => (
<li
key={p.properties.id}
key={index}
className={
selected === p.properties.id
selected === index
? 'rounded border-2 border-emerald-700 p-3'
: 'rounded border p-3'
}
@@ -159,18 +193,17 @@ export default function InteractiveInvestmentMap({
<button
type="button"
className="text-left font-bold underline"
aria-pressed={selected === p.properties.id}
aria-pressed={selected === index}
onClick={() => {
const marker = markers.current.get(
p.properties.id,
);
const marker = markers.current.get(index);
if (marker) {
map.current?.setView(
marker.getLatLng(),
14,
{ animate: false },
);
marker.openPopup();
setSelected(p.properties.id);
setSelected(index);
}
}}
>
@@ -179,12 +212,28 @@ export default function InteractiveInvestmentMap({
<p>
{p.properties.category} · {p.properties.location}
</p>
<p>
Fonte:{' '}
{p.properties.source_reference ?? 'Não indicada'} ·
Data:{' '}
{p.properties.source_date ?? 'Não documentada'}
</p>
<a className="underline" href={p.properties.url}>
Abrir ficha
</a>
</li>
))}
</ul>
{areas.map((area, index) => (
<p key={index}>
<a className="underline" href={area.url}>
{area.title}
</a>{' '}
· Polígono cadastrado, limites oficiais por verificar ·
Fonte: {area.source_reference ?? 'Não indicada'} · Data:{' '}
{area.source_date ?? 'Não documentada'}
</p>
))}
</section>
);
}
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import InteractiveInvestmentMap, {
type InvestmentMapFeature,
type InvestmentMapArea,
} from './interactive-investment-map';
export default function InvestmentMapPanel({
@@ -14,10 +15,12 @@ export default function InvestmentMapPanel({
{ id: number; title: string; url: string }[]
>([]);
const [error, setError] = useState('');
const [areas, setAreas] = useState<InvestmentMapArea[]>([]);
useEffect(() => {
const controller = new AbortController();
const params = new URLSearchParams(JSON.parse(key));
setPoints(null);
setAreas([]);
fetch(`/api/v1/investment/map?${params}`, {
signal: controller.signal,
headers: { Accept: 'application/json' },
@@ -29,6 +32,7 @@ export default function InvestmentMapPanel({
.then((data) => {
if (controller.signal.aborted) return;
setPoints(data.features);
setAreas(data.areas ?? []);
setMissing(data.unlocated ?? []);
setError(
data.truncated
@@ -44,12 +48,13 @@ export default function InvestmentMapPanel({
return (
<section className="space-y-3">
{error && <p role="alert">{error}</p>}
<InteractiveInvestmentMap points={points} />
<InteractiveInvestmentMap points={points} areas={areas} />
{missing.length > 0 && (
<div>
<p>
{missing.length} investimentos aguardam coordenadas do
local:
{missing.length} investimentos sem ponto público
utilizável. Consulte as fichas para localizações
documentadas sem coordenadas:
</p>
<ul>
{missing.map((record) => (
@@ -1,4 +1,8 @@
import { Link } from '@inertiajs/react';
import TerritorialSources, {
type TerritorialSourceEvidence,
} from './territorial-source-evidence';
import type { GeoJsonObject } from 'geojson';
import InteractiveInvestmentMap from './interactive-investment-map';
export type DocumentedLocation = {
@@ -6,15 +10,24 @@ export type DocumentedLocation = {
name: string;
role: string;
source_reference?: string;
source_date?: string | null;
boundaries?: {
geometry_id: string;
source_reference: string | null;
source_date: string | null;
geometry: GeoJsonObject;
}[];
location_type: string;
path: { id: string; name: string; location_type: string }[];
coordinates: {
geometry_id: string;
source_reference: string;
source_reference: string | null;
source_date?: string | null;
geometry: { coordinates: [number, number] };
}[];
};
export type StructureGeography = {
source_evidence?: TerritorialSourceEvidence[];
locations: DocumentedLocation[];
members: {
id: string;
@@ -44,6 +57,7 @@ export type StructureGeography = {
};
const roles: Record<string, string> = {
opportunity: 'Implantação cadastrada da oportunidade',
primary: 'Localização principal',
secondary: 'Localização secundária',
impact: 'Área de impacto',
@@ -85,11 +99,13 @@ export function LocationList({
<p key={point.geometry_id}>
Coordenadas documentadas (
{location.location_type === 'site'
? 'local do projecto'
? 'local cadastrado; não confirma a implantação da oportunidade'
: 'referência territorial; não representa a implantação'}
): {point.geometry.coordinates[1]},{' '}
{point.geometry.coordinates[0]} · Fonte:{' '}
{point.source_reference}
{point.source_reference ?? 'Não indicada'}
{' · Data da fonte: '}
{point.source_date ?? 'Não documentada'}
</p>
))}
{!location.coordinates.length && (
@@ -98,7 +114,20 @@ export function LocationList({
<p className="text-sm">
Fonte da relação:{' '}
{location.source_reference ?? 'Não indicada'}
{' · Data da fonte: '}
{location.source_date ?? 'Não documentada'}
</p>
{location.boundaries?.map((boundary) => (
<p key={boundary.geometry_id}>
Polígono cadastrado · Fonte:{' '}
{boundary.source_reference ?? 'Não indicada'} ·
Data: {boundary.source_date ?? 'Não documentada'}. O
cadastro não certifica limites oficiais.
</p>
))}
{!location.boundaries?.length && (
<p>Polígono não documentado.</p>
)}
<Link
className="underline"
href={`/oportunidades?geo_location_id=${location.id}`}
@@ -165,6 +194,7 @@ export default function StructureGeographyPanel({
: 'Localização documentada do projecto'}
</h2>
<LocationList locations={geography.locations} />
<TerritorialSources sources={geography.source_evidence} />
{!geography.locations.length && (
<p>
{cluster
@@ -178,7 +208,27 @@ export default function StructureGeographyPanel({
um ponto único atribuído ao cluster.
</p>
)}
<InteractiveInvestmentMap points={points} />
<InteractiveInvestmentMap
points={points}
areas={[
...items.flatMap((item) =>
(item.location.boundaries ?? []).map((area) => ({
...area,
title: `${roles[item.location.role] ?? item.location.role} · ${item.title}`,
url: item.url,
})),
),
...(cluster
? geography.locations.flatMap((location) =>
(location.boundaries ?? []).map((area) => ({
...area,
title: `Abrangência do cluster · ${location.name}`,
url: `/carteira/${dossierId}`,
})),
)
: []),
]}
/>
{!points.length && (
<p>
Sem coordenadas documentadas dos locais. Nenhum ponto foi
@@ -0,0 +1,52 @@
export type TerritorialSourceEvidence = {
association: string;
role: string;
source_reference: string | null;
source_quote: string | null;
supports: string;
traceability: string;
official_source_accepted: boolean;
existing_validation: string | null;
};
export default function TerritorialSources({
sources,
}: {
sources?: TerritorialSourceEvidence[];
}) {
if (!sources?.length) return null;
return (
<section className="space-y-3 rounded border p-4">
<h2 className="text-xl font-bold">
Fundamento documental das associações
</h2>
<p>
Fontes oficiais identificadas são aceites; recuperar uma
referência não retira a aceitação existente. O fundamento
aplica-se à informação indicada, sem deduzir implantação,
disponibilidade ou capacidade.
</p>
{sources.map((source, i) => (
<article className="border-t pt-3" key={i}>
<h3 className="font-semibold">
{source.role} — {source.association}
</h3>
<p>
Fonte:{' '}
{source.source_reference ??
'ligação ao exemplar por recuperar'}
</p>
<p>{source.source_quote ?? source.supports}</p>
<p>
{source.official_source_accepted
? 'Fonte oficial identificada e aceite.'
: 'Aceitação existente preservada; ponte ao exemplar oficial por recuperar. Esta referência não recebe outra classificação de proveniência.'}{' '}
Rastreabilidade: {source.traceability}. Estado
registado:{' '}
{source.existing_validation ?? 'sem campo específico'};
preservado.
</p>
</article>
))}
</section>
);
}
@@ -0,0 +1,155 @@
import { useForm } from '@inertiajs/react';
export type TraceabilityPlan = {
hash: string;
code_matches: boolean;
backup_ready: boolean;
applied: boolean;
plan: {
target: { database: string; port: number };
rows: {
key: string;
reference: string;
association: string;
role: string;
source_reference: string | null;
source_quote: string | null;
supports: string;
traceability: string;
existing_validation: string | null;
effect: string;
}[];
updates: {
id: string;
reference: string;
current: {
source_reference: string | null;
evidence_reference: string | null;
};
values: { source_reference: string; evidence_reference: string };
source_quote: string;
effect: string;
}[];
pending: { reference: string; reason: string }[];
};
};
export default function TerritorialTraceabilityPlan({
preview,
}: {
preview: TraceabilityPlan;
}) {
const form = useForm({
hash: preview.hash,
operation: 'traceability',
reverse: false,
});
return (
<section className="space-y-4 rounded border p-5">
<h2 className="text-xl font-bold">
V2 — aplicar rastreabilidade territorial
</h2>
<p>
Destino: {preview.plan.target.database}:
{preview.plan.target.port}. Fontes oficiais identificadas são
aceites; não se repete validação da proveniência. Estados
existentes são preservados.
</p>
<p>
{preview.plan.rows.length} associações/fontes;{' '}
{preview.plan.updates.length} atualizações de referências.
Nenhuma nova implantação ou relação por proximidade.
</p>
<p role="status">
Backup/restauro/reversão:{' '}
{preview.backup_ready
? 'verificados (validade 24 horas)'
: 'pendentes ou expirados'}{' '}
· Código:{' '}
{preview.code_matches
? 'corresponde ao plano'
: 'plano deve ser atualizado'}{' '}
· Aplicação: {preview.applied ? 'aplicada' : 'não aplicada'}
</p>
<form
onSubmit={(e) => {
e.preventDefault();
form.transform(() => ({
hash: preview.hash,
operation: 'traceability',
reverse: preview.applied,
}));
form.post('/management/relationship-repair', {
preserveScroll: true,
});
}}
>
<button
disabled={
form.processing ||
!preview.backup_ready ||
!preview.code_matches
}
className="rounded bg-emerald-800 px-4 py-3 text-white disabled:opacity-50"
>
{preview.applied
? 'Reverter este incremento de rastreabilidade'
: 'Aplicar rastreabilidade territorial V2'}
</button>
{Object.values(form.errors).map((error) => (
<p role="alert" key={error}>
{error}
</p>
))}
</form>
<h3 className="font-bold">Alterações aos campos existentes</h3>
{preview.plan.updates.map((update) => (
<article className="border-t py-3" key={update.id}>
<h4 className="font-bold">{update.reference}</h4>
<p>
Referência atual:{' '}
{update.current.source_reference ?? 'não preenchida'};
evidência atual:{' '}
{update.current.evidence_reference ?? 'não preenchida'}.
</p>
<p>
Proposta: {update.values.source_reference}; dossier{' '}
{update.values.evidence_reference}.
</p>
<p>Fundamento: {update.source_quote}</p>
<p>{update.effect}</p>
</article>
))}
<h3 className="font-bold">Fundamento por associação</h3>
{preview.plan.rows.map((row) => (
<article className="border-t py-3" key={row.key}>
<h3 className="font-bold">
{row.reference} — {row.association} ({row.role})
</h3>
<p>
Atual:{' '}
{row.existing_validation ??
'sem campo de validação nesta associação'}
; estado preservado.
</p>
<p>
Proposta: ligar a referência e o fundamento. Fonte:{' '}
{row.source_reference ?? 'por recuperar no acervo'}
</p>
<p>Citação/âmbito: {row.source_quote ?? row.supports}</p>
<p>
Rastreabilidade: {row.traceability} · Efeito:{' '}
{row.effect}
</p>
</article>
))}
<h3 className="font-bold">
Pendências de rastreabilidade (não invalidam dados)
</h3>
{preview.plan.pending.map((row, i) => (
<p key={i}>
{row.reference}: {row.reason}
</p>
))}
</section>
);
}
+26 -1
View File
@@ -1,6 +1,7 @@
import { Link, router, useForm, usePage } from '@inertiajs/react';
import { useEffect, useRef, useState } from 'react';
import Map from '@/components/interactive-investment-map';
import type { InvestmentMapArea } from '@/components/interactive-investment-map';
import {
TerritoryLevel,
type TerritoryOption,
@@ -57,6 +58,7 @@ export default function Catalogue({
q: String(filters.q ?? ''),
dimension: String(filters.dimension ?? ''),
view: String(filters.view ?? 'grid'),
territory_basis: String(filters.territory_basis ?? 'all'),
sector: String(filters.sector ?? ''),
province: String(filters.province ?? ''),
district: String(filters.district ?? ''),
@@ -91,6 +93,7 @@ export default function Catalogue({
const [selected, setSelected] = useState<number[]>([]);
const [comparison, setComparison] = useState<{ record: Opportunity }[]>([]);
const [points, setPoints] = useState<Feature[] | null>(null);
const [areas, setAreas] = useState<InvestmentMapArea[]>([]);
const [unlocated, setUnlocated] = useState<
{ id: number; title: string; location: string; url: string }[]
>([]);
@@ -147,6 +150,7 @@ export default function Catalogue({
);
}
setPoints(null);
setAreas([]);
setUnlocated([]);
setTruncated(false);
setError('');
@@ -154,6 +158,7 @@ export default function Catalogue({
.then((data) => {
if (active) {
setPoints(data.features);
setAreas(data.areas ?? []);
setUnlocated(data.unlocated ?? []);
setTruncated(Boolean(data.truncated));
}
@@ -551,6 +556,26 @@ export default function Catalogue({
/>
</Field>
</section>
<Field label="Base da relação territorial">
<select
className={input}
value={form.data.territory_basis}
onChange={(e) =>
form.setData('territory_basis', e.target.value)
}
>
<option value="all">
Implantação e contexto associado
</option>
<option value="direct">
Implantação declarada da oportunidade
</option>
<option value="context">
Apenas contexto associado; implantação por
documentar
</option>
</select>
</Field>
<label className="flex gap-2">
<input
type="checkbox"
@@ -1060,7 +1085,7 @@ export default function Catalogue({
</Link>
</div>
{error && <p role="alert">{error}</p>}
{!error && <Map points={points} />}
{!error && <Map points={points} areas={areas} />}
{truncated && (
<p role="alert">
O mapa atingiu o limite de 1.000 registos; refine os
+25 -2
View File
@@ -1,8 +1,11 @@
import { Link, router, usePage } from '@inertiajs/react';
import { useState } from 'react';
import InvestmentMapPanel from '@/components/investment-map-panel';
import StructureGeographyPanel from '@/components/structure-geography';
import StructureGeographyPanel, {
LocationList,
} from '@/components/structure-geography';
import PartnershipSourceLinks from '@/components/partnership-source-links';
import TerritorialSources from '@/components/territorial-source-evidence';
import { Frame, card, button, money, groupLabels } from './shared';
import type { Opportunity, Context } from './shared';
export default function Detail({
@@ -48,11 +51,31 @@ export default function Detail({
</p>
)}
<p className="text-xl">{r.summary}</p>
<TerritorialSources sources={c.source_evidence} />
<section className={card + ' space-y-3'}>
<h2 className="text-xl font-bold">
Implantação declarada da oportunidade
</h2>
<p>
As relações cadastradas não confirmam disponibilidade ou
capacidade. Locais dos projetos associados, áreas de impacto
e abrangência dos clusters são apresentados separadamente.
</p>
<LocationList locations={c.opportunity_locations ?? []} />
{!c.opportunity_locations?.length && (
<p>
Implantação estruturada não documentada. O contexto
associado abaixo não confirma onde a oportunidade será
implantada.
</p>
)}
</section>
<InvestmentMapPanel filters={{ opportunity_id: r.id }} />
{c.primary_territory_path &&
c.primary_territory_path.length > 0 && (
<p>
Localização:{' '}
Território principal cadastrado (implantação por
confirmar):{' '}
{c.primary_territory_path
.map((node) => node.name)
.join(' → ')}
+7 -1
View File
@@ -1,7 +1,11 @@
import { Head, Link, usePage } from '@inertiajs/react';
import type { ReactNode } from 'react';
import { PortalMenu } from '@/layouts/portal-navigation';
import type { StructureGeography } from '@/components/structure-geography';
import type { TerritorialSourceEvidence } from '@/components/territorial-source-evidence';
import type {
StructureGeography,
DocumentedLocation,
} from '@/components/structure-geography';
export type Option = {
id: number;
@@ -47,6 +51,8 @@ export type Opportunity = {
last_validated_at?: string | null;
};
export type Context = {
source_evidence?: TerritorialSourceEvidence[];
opportunity_locations?: DocumentedLocation[];
primary_territory_path?: {
id: string;
name: string;
+6
View File
@@ -8,6 +8,9 @@ import {
import { useState } from 'react';
import { Frame, Field, input, button, card, api } from './shared';
import PartnershipSourceLinks from '@/components/partnership-source-links';
import TerritorialSources, {
type TerritorialSourceEvidence,
} from '@/components/territorial-source-evidence';
import ZoneRelations from './zone-relations';
import ZoneMap from './zone-map';
import ZoneAttachments from './zone-attachments';
@@ -124,6 +127,7 @@ type Props = {
validation_status: string;
}[];
record: Zone;
territorialSources?: TerritorialSourceEvidence[];
management: boolean;
zoneExtensionsAvailable?: boolean;
source: Source | null;
@@ -186,6 +190,7 @@ function Editor({
relations,
geometries,
record: z,
territorialSources,
management,
zoneExtensionsAvailable = false,
source,
@@ -268,6 +273,7 @@ function Editor({
}
return (
<Frame title={z.name}>
<TerritorialSources sources={territorialSources} />
{!management && <PartnershipSourceLinks type="zone" id={z.id} />}
<p>
{z.code} · {z.category.toUpperCase()} · {z.validation_status} ·{' '}
@@ -1,5 +1,8 @@
import { Link, useForm } from '@inertiajs/react';
import { Management } from './shared';
import TerritorialTraceabilityPlan, {
type TraceabilityPlan,
} from '@/components/territorial-traceability-plan';
type Plan = {
target: { database: string; port: number };
@@ -149,16 +152,21 @@ export default function RelationshipRepair({
territorial,
history,
unproposed,
traceability,
}: {
hash: string;
plan: Plan;
territorial: Territory[];
history: History[];
unproposed: Unproposed[];
traceability?: TraceabilityPlan | null;
}) {
const form = useForm({ hash });
return (
<Management title="Associações documentadas de projectos âncora e clusters">
{traceability && (
<TerritorialTraceabilityPlan preview={traceability} />
)}
<p>
Pré-visualização para {plan.target.database}:{plan.target.port}.
A execução verifica o backup e o restauro, a versão dos dossiers
+52 -3
View File
@@ -12,6 +12,7 @@ use App\Services\InvestmentCatalogue;
use App\Services\InvestmentRelations;
use App\Services\InvestmentRelationshipRepair;
use App\Services\InvestmentTerritorialValidation;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@@ -104,7 +105,7 @@ class InvestmentRelationshipTest extends TestCase
$this->get('/carteira/'.$f['dossier']->id)->assertNotFound();
}
public function test_ambiguous_multiple_sites_do_not_choose_an_arbitrary_opportunity_marker(): void
public function test_multiple_documented_sites_are_shown_separately_without_choosing_one(): void
{
$f = $this->fixture();
$id = (string) Str::uuid();
@@ -112,11 +113,59 @@ class InvestmentRelationshipTest extends TestCase
DB::table('project_locations')->insert(['id' => (string) Str::uuid(), 'project_id' => $f['project']->id, 'geo_location_id' => $id, 'role' => 'secondary', 'data_classification' => 'public']);
DB::table('geo_geometries')->insert(['id' => (string) Str::uuid(), 'geo_location_id' => $id, 'geometry_type' => 'point', 'data_classification' => 'public', 'geom' => DB::raw('ST_SetSRID(ST_MakePoint(32.6,-25.8),4326)')]);
$map = app(InvestmentCatalogue::class)->map([]);
$this->assertSame(0, $map['georeferenced_count']);
$this->assertCount(1, $map['unlocated']);
$this->assertSame(1, $map['georeferenced_count']);
$this->assertCount(0, $map['unlocated']);
$this->assertCount(2, $map['features']);
$this->assertSame(2, $map['marker_count']);
$this->assertCount(2, app(InvestmentRelations::class)->projectLocations($f['project']->id));
}
public function test_direct_context_impact_and_missing_geography_remain_distinct(): void
{
$f = $this->fixture();
$catalogue = app(InvestmentCatalogue::class);
$this->assertEmpty($catalogue->context($f['opportunity']->id)['opportunity_locations']);
$this->assertSame([$f['opportunity']->id], $catalogue->query(['territory_basis' => 'context'])->pluck('id')->all());
$this->assertSame([], $catalogue->query(['territory_basis' => 'direct'])->pluck('id')->all());
DB::table('opportunity_locations')->insert(['opportunity_id' => $f['opportunity']->id, 'geo_location_id' => $f['ids']['district']]);
$location = $catalogue->context($f['opportunity']->id)['opportunity_locations'][0];
$this->assertSame('opportunity', $location['role']);
$this->assertSame([$f['opportunity']->id], $catalogue->query(['q' => 'QA district'])->pluck('id')->all());
$this->assertNull($location['source_reference']);
$this->assertEmpty($location['coordinates']);
$this->assertSame([], $catalogue->query(['territory_basis' => 'context'])->pluck('id')->all());
$this->assertSame([$f['opportunity']->id], $catalogue->query(['territory_basis' => 'direct', 'geo_location_id' => $f['ids']['district']])->pluck('id')->all());
$this->assertSame([], $catalogue->query(['territory_basis' => 'direct', 'geo_location_id' => $f['ids']['site']])->pluck('id')->all());
DB::table('project_locations')->update(['role' => 'impact']);
$this->assertSame('impact', $catalogue->context($f['opportunity']->id)['projects'][0]->geography['locations'][0]['role']);
$this->assertSame(0, $catalogue->map([])['georeferenced_count']);
DB::table('opportunity_locations')->delete();
DB::table('opportunity_projects')->delete();
$this->assertEmpty($catalogue->context($f['opportunity']->id)['opportunity_locations']);
$this->assertSame(1, $catalogue->query([])->count());
$this->assertCount(1, $catalogue->map([])['unlocated']);
}
public function test_invalid_coordinates_are_rejected_and_polygon_provenance_is_preserved(): void
{
$f = $this->fixture();
DB::table('opportunity_locations')->insert(['opportunity_id' => $f['opportunity']->id, 'geo_location_id' => $f['ids']['site']]);
DB::table('geo_geometries')->insert(['id' => (string) Str::uuid(), 'geo_location_id' => $f['ids']['site'], 'geometry_type' => 'polygon', 'data_classification' => 'public',
'geom' => DB::raw("ST_GeomFromText('POLYGON((32 -26,33 -26,33 -25,32 -25,32 -26))',4326)"), 'source_system' => 'QA boundary', 'properties' => json_encode(['source_date' => '2026-10-08'])]);
$context = app(InvestmentCatalogue::class)->context($f['opportunity']->id);
$this->assertSame('2026-10-08', $context['opportunity_locations'][0]['boundaries'][0]['source_date']);
$this->assertSame('opportunity_site', app(InvestmentCatalogue::class)->map([])['features'][0]['properties']['location_basis']);
$this->assertCount(1, app(InvestmentCatalogue::class)->map([])['areas']);
try {
DB::transaction(fn () => DB::table('geo_geometries')->where('geo_location_id', $f['ids']['site'])->where('geometry_type', 'point')->update(['geom' => DB::raw('ST_SetSRID(ST_MakePoint(32.7,95),4326)')]));
$this->fail('Invalid coordinates must be rejected by the existing database guard.');
} catch (QueryException $error) {
$this->assertSame('23514', $error->getCode());
}
$this->assertSame(1, app(InvestmentCatalogue::class)->map([])['georeferenced_count']);
$this->assertCount(1, app(InvestmentCatalogue::class)->context($f['opportunity']->id)['opportunity_locations'][0]['coordinates']);
}
public function test_repair_requires_authenticated_management_is_transactional_and_replays_without_duplicates(): void
{
$f = $this->fixture();
@@ -0,0 +1,106 @@
<?php
namespace Tests\Feature;
use App\Models\EconomicCluster;
use App\Models\InvestmentDossier;
use App\Models\Project;
use App\Models\User;
use App\Services\InvestmentTerritorialTraceability;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Tests\TestCase;
class InvestmentTerritorialTraceabilityTest extends TestCase
{
use RefreshDatabase;
private string $directory;
protected function setUp(): void
{
parent::setUp();
$this->directory = storage_path('framework/testing/territorial-'.Str::uuid());
mkdir($this->directory, 0700, true);
config(['investment.traceability_directory' => $this->directory]);
}
protected function tearDown(): void
{
foreach (glob($this->directory.'/*') ?: [] as $file) {
unlink($file);
}
rmdir($this->directory);
parent::tearDown();
}
private function fixture(): array
{
$project = Project::factory()->create();
$cluster = EconomicCluster::create(['code' => 'QA-TRACE', 'name' => 'Synthetic QA cluster', 'sector' => 'QA', 'status' => 'planned']);
$dossier = InvestmentDossier::create(['cluster_id' => $cluster->id, 'title' => 'QA synthetic source', 'source' => 'Synthetic fixture only', 'content' => [], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'approval_basis' => 'document_owner', 'approval_evidence' => 'QA synthetic disclosure']);
$id = (string) Str::uuid();
DB::table('cluster_projects')->insert(['id' => $id, 'cluster_id' => $cluster->id, 'project_id' => $project->id, 'role' => 'anchor', 'source_system' => 'QA']);
$row = DB::table('cluster_projects')->where('id', $id)->first();
$plan = ['schema' => 1, 'target' => ['database' => config('database.connections.pgsql.database'), 'port' => 55441], 'code_hash' => app(InvestmentTerritorialTraceability::class)->fingerprint(),
'rows' => [['kind' => 'economic_clusters', 'record_id' => $cluster->id, 'association' => $project->id, 'role' => 'context', 'public' => true, 'dossier_id' => $dossier->id, 'source_reference' => 'QA synthetic source', 'source_quote' => 'QA anchor', 'supports' => 'Existing membership only', 'traceability' => 'quote recovered', 'official_source_accepted' => false, 'existing_validation' => $row->validation_status]],
'guards' => [['table' => 'cluster_projects', 'id' => $id, 'expected' => ['version_no' => $row->version_no, 'validation_status' => $row->validation_status, 'source_reference' => null]]],
'updates' => [['table' => 'cluster_projects', 'id' => $id, 'values' => ['source_reference' => 'QA synthetic source', 'evidence_reference' => $dossier->id]]]];
$bytes = json_encode($plan, JSON_THROW_ON_ERROR);
file_put_contents($this->directory.'/territory-activation-plan.json', $bytes);
file_put_contents($this->directory.'/qa-backup-fixture', 'QA fixture, not active backup evidence');
$hash = hash('sha256', $bytes);
$proof = ['restore_verified' => true, 'reversal_verified' => true, 'target' => $plan['target'], 'plan_hash' => $hash, 'code_hash' => $plan['code_hash'], 'verified_at' => gmdate('c'), 'dump_path' => $this->directory.'/qa-backup-fixture', 'sha256' => hash_file('sha256', $this->directory.'/qa-backup-fixture')];
file_put_contents($this->directory.'/territory-backup-proof.json', json_encode($proof, JSON_THROW_ON_ERROR));
$manager = User::factory()->create(['email' => 'territory-manager@example.invalid']);
config(['portal.managers' => [$manager->email]]);
return compact('hash', 'manager', 'id', 'cluster', 'dossier', 'row');
}
public function test_authenticated_application_is_idempotent_and_reversible_without_revalidating_sources(): void
{
$f = $this->fixture();
$payload = ['operation' => 'traceability', 'hash' => $f['hash'], 'reverse' => false];
$this->actingAs($f['manager'])->postJson('/management/relationship-repair', $payload)->assertOk()->assertJsonPath('changed', 1);
$this->postJson('/management/relationship-repair', $payload)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('changed', 0);
$service = app(InvestmentTerritorialTraceability::class);
$this->assertCount(1, $service->sources('economic_clusters', $f['cluster']->id));
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'validation_status' => $f['row']->validation_status, 'source_reference' => 'QA synthetic source']);
$f['dossier']->update(['publication_status' => 'draft']);
$this->assertSame([], $service->sources('economic_clusters', $f['cluster']->id));
$this->postJson('/management/relationship-repair', [...$payload, 'reverse' => true])->assertOk()->assertJsonPath('changed', 1);
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'version_no' => $f['row']->version_no + 2, 'source_reference' => null, 'evidence_reference' => null, 'validation_status' => $f['row']->validation_status]);
$this->assertSame([], $service->sources('economic_clusters', $f['cluster']->id));
$this->assertSame(2, DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->count());
$this->postJson('/management/relationship-repair', $payload)->assertConflict();
}
public function test_permissions_and_conflicting_snapshot_prevent_writes_and_show_field_error(): void
{
$f = $this->fixture();
$payload = ['operation' => 'traceability', 'hash' => $f['hash'], 'reverse' => false];
$this->actingAs(User::factory()->create())->postJson('/management/relationship-repair', $payload)->assertForbidden();
DB::table('cluster_projects')->where('id', $f['id'])->increment('version_no');
$this->actingAs($f['manager'])->postJson('/management/relationship-repair', $payload)->assertConflict();
$this->from('/management/relationship-repair')->post('/management/relationship-repair', $payload)->assertRedirect('/management/relationship-repair')->assertSessionHasErrors('operation');
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'source_reference' => null]);
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'territory-traceability-v2')->count());
}
public function test_expired_backup_and_modified_code_proof_require_updated_plan(): void
{
$f = $this->fixture();
$proofFile = $this->directory.'/territory-backup-proof.json';
$proof = json_decode(file_get_contents($proofFile), true);
$proof['verified_at'] = gmdate('c', time() - 86401);
file_put_contents($proofFile, json_encode($proof));
$this->actingAs($f['manager'])->postJson('/management/relationship-repair', ['operation' => 'traceability', 'hash' => $f['hash'], 'reverse' => false])->assertUnprocessable();
$proof['verified_at'] = gmdate('c');
$proof['code_hash'] = 'wrong';
file_put_contents($proofFile, json_encode($proof));
$this->assertFalse(app(InvestmentTerritorialTraceability::class)->preview()['backup_ready']);
$this->assertDatabaseHas('cluster_projects', ['id' => $f['id'], 'source_reference' => null]);
}
}
@@ -0,0 +1,275 @@
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const root = path.resolve(__dirname, '..');
const dir = path.join(root, 'storage/app/private/investmoz-v2');
const audit = JSON.parse(
fs.readFileSync(path.join(dir, 'territory-audit.json'), 'utf8'),
);
const reconciliation = JSON.parse(
fs.readFileSync(
path.join(dir, 'territory-source-reconciliation.json'),
'utf8',
),
);
if (
audit.identity.read_only !== 'on' ||
reconciliation.captured_at !== audit.captured_at
)
throw new Error('Matching read-only source reconciliation required');
const plan = {
schema: 1,
prepared_at: new Date().toISOString(),
target: {
database: audit.identity.database,
port: Number(audit.identity.port),
},
code_hash: '',
rows: [],
updates: [],
guards: [],
pending: [],
new_relationships: [],
};
const guardIds = new Set();
const guard = (table, record, fields) => {
const key = table + ':' + record.id;
if (guardIds.has(key)) return;
guardIds.add(key);
plan.guards.push({
table,
id: record.id,
expected: Object.fromEntries(
fields
.filter((field) => field in record)
.map((field) => [field, record[field]]),
),
});
};
const publicDossiers = audit.dossiers.filter(
(d) => d.publication_status === 'published',
);
for (const entity of audit.records) {
const record = entity.record;
const reference = record.code || record.reference || String(record.id);
const source = reconciliation.records.find(
(r) => r.kind === entity.kind && r.id === entity.id,
);
guard(entity.kind, record, [
'version_no',
'territory_version',
'metadata',
'source_evidence',
'validation_status',
'publication_status',
'status',
'primary_territory_id',
'deleted_at',
]);
const dossiers = audit.dossiers.filter(
(d) => d.project_id === record.id || d.cluster_id === record.id,
);
for (const dossier of dossiers)
guard('investment_dossiers', dossier, [
'version_no',
'source',
'content',
'publication_status',
]);
const visible =
entity.kind === 'opportunities'
? record.status === 'published'
: entity.kind === 'investment_zones'
? record.publication_status === 'published'
: dossiers.some((d) => d.publication_status === 'published');
const metadata = JSON.parse(record.metadata || '{}');
const legal = source.legal_source;
if (legal) guard('investment_legal_sources', legal, Object.keys(legal));
const evidence =
source.official_references[0] || source.non_official_references[0];
const row = (association, role, props = {}) => {
plan.rows.push({
key: `${entity.kind}:${entity.id}:${association}:${role}`,
kind: entity.kind,
record_id: entity.id,
reference,
association,
role,
public: visible,
source_reference: legal
? `${legal.diploma} — ${legal.url}`
: evidence
? `${evidence.title} — ${evidence.url}`
: dossiers[0]?.source || null,
source_quote: null,
supports:
evidence?.supports ||
'Origem oficial aceite pelo responsável; ligação ao exemplar a recuperar onde ausente.',
existing_validation: record.validation_status ?? null,
official_source_accepted: Boolean(
legal || source.official_references.length,
),
traceability: source.traceability,
effect: 'Ficha mostra fundamento e pendência específica; identidade, geometria, filtros e validações preservados.',
...props,
});
};
row('registo', 'proveniência');
const locations = entity.relations.own || [];
for (const location of locations) {
if (location.id)
guard(
entity.kind === 'projects'
? 'project_locations'
: 'investment_zone_locations',
location,
Object.keys(location),
);
const saved =
metadata.documented_location_sources?.[
location.geo_location_id + ':' + location.role
];
row(
location.geo_location_id,
location.role || location.scope_type || 'implantação',
{
public_location_id: location.geo_location_id,
source_reference:
saved?.source_reference ||
location.source_reference ||
legal?.url ||
evidence?.url ||
null,
source_quote: saved?.source_quote || null,
supports:
saved?.reason ||
evidence?.supports ||
'Ligação ao documento utilizado por recuperar; dado preservado.',
dossier_id: saved?.dossier_id || null,
},
);
}
if (
record.primary_territory_id &&
!locations.some(
(l) => l.geo_location_id === record.primary_territory_id,
)
)
row(
record.primary_territory_id,
entity.kind === 'opportunities'
? 'implantação cadastrada'
: 'território principal',
{
public_location_id: record.primary_territory_id,
supports:
'Associação já existente; nomes administrativos provenientes do workbook cartografia-moz. Não se atribui automaticamente o PDF INE a este vínculo.',
},
);
for (const coverage of metadata.documented_coverage || [])
row(coverage.id, 'abrangência', {
source_reference: coverage.source_reference || null,
source_quote: coverage.source_quote || null,
public_location_id: coverage.id,
public: visible && coverage.data_classification === 'public',
});
if (entity.kind === 'economic_clusters') {
const dossier = publicDossiers.find((d) => d.cluster_id === record.id);
const content = JSON.parse(dossier?.content || '{}');
for (const member of entity.relations.members || []) {
const project = audit.records.find(
(r) => r.kind === 'projects' && r.id === member.project_id,
);
const quote = content.anchors || '';
const explicit =
project &&
new RegExp(
'(^|[^A-Za-z0-9])' +
project.record.code +
'([^A-Za-z0-9]|$)',
).test(quote);
guard('cluster_projects', member, Object.keys(member));
row(member.project_id, 'contexto — membro ' + member.role, {
association_id: member.id,
dossier_id: dossier?.id || null,
source_reference:
member.source_reference ||
(explicit ? dossier.source : null),
source_quote: explicit ? quote : null,
existing_validation: member.validation_status,
official_source_accepted: false,
traceability: explicit
? 'citação recuperada; ponte ao exemplar oficial por recuperar'
: 'rastreabilidade por recuperar',
effect: 'Citação da relação cluster–projeto passa a ser visível; não muda validação nem cria implantação da oportunidade.',
});
if (
explicit &&
!member.source_reference &&
!member.evidence_reference
)
plan.updates.push({
table: 'cluster_projects',
id: member.id,
reference: `${reference} → ${project.record.code}`,
current: {
source_reference: null,
evidence_reference: null,
},
values: {
source_reference: dossier.source,
evidence_reference: dossier.id,
},
source_quote: quote,
effect: 'Relação existente reutilizada; apenas referências preenchidas.',
});
}
}
if (source.traceability !== 'official_reference_identified')
plan.pending.push({
reference,
reason: 'Ponte ao exemplar oficial não identificada no cadastro revisto; recuperar sem desvalidar.',
});
if (
entity.kind === 'opportunities' &&
!(entity.relations.projects || []).length &&
!(entity.relations.clusters || []).length
)
plan.pending.push({
reference,
reason: 'Sem correspondência documental explícita a uma âncora/cluster nas fontes já reconciliadas; não criar ligação por setor ou proximidade.',
});
}
const files = [
'app/Services/InvestmentTerritorialTraceability.php',
'app/Http/Controllers/InvestmentRelationshipRepairController.php',
'resources/js/components/territorial-traceability-plan.tsx',
'tools/investmoz-v2-prepare-territorial-plan.cjs',
];
plan.code_hash = crypto
.createHash('sha256')
.update(
files
.map((file) =>
crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(root, file)))
.digest('hex'),
)
.join(''),
)
.digest('hex');
fs.writeFileSync(
path.join(dir, 'territory-activation-plan.json'),
JSON.stringify(plan, null, 2),
);
console.log(
JSON.stringify({
records: audit.records.length,
associations: plan.rows.length,
updates: plan.updates.length,
pending: plan.pending.length,
new_relationships: 0,
target: plan.target,
}),
);
@@ -0,0 +1,183 @@
const fs = require('node:fs');
const path = require('node:path');
function reconcile(item, audit) {
const row = item.record;
const metadata = JSON.parse(row.metadata || '{}');
const evidence = JSON.parse(row.source_evidence || '[]');
const legal =
(audit.legal_sources || []).find(
(source) => source.id === row.legal_source_id,
) || null;
const dossiers = audit.dossiers.filter(
(d) => d.project_id === row.id || d.cluster_id === row.id,
);
const officialReferences = evidence.filter((source) =>
['government', 'institutional'].includes(source.source_type),
);
const documentaryRelations = Object.entries(
metadata.documented_location_sources || {},
).map(([key, source]) => ({ key, ...source }));
return {
kind: item.kind,
id: row.id,
reference: row.code || row.reference || String(row.id),
existing_record_validation: row.validation_status ?? null,
existing_legal_validation: legal?.validation_status ?? null,
legal_source: legal,
official_references: officialReferences,
non_official_references: evidence.filter(
(source) =>
!['government', 'institutional'].includes(source.source_type),
),
documentary_relations: documentaryRelations,
dossiers: dossiers.map((d) => ({
id: d.id,
source: d.source,
version: d.version_no,
})),
traceability:
legal || officialReferences.length
? 'official_reference_identified'
: documentaryRelations.length || dossiers.length
? 'documentary_bridge_to_official_source_to_recover'
: 'official_source_link_not_identified_in_records_reviewed',
official_validation_newly_granted: false,
validation_revoked: false,
calculation_activated: false,
};
}
module.exports = { reconcile };
if (require.main === module) {
const root = path.resolve(__dirname, '..');
const privateDir = path.join(root, 'storage/app/private/investmoz-v2');
const audit = JSON.parse(
fs.readFileSync(path.join(privateDir, 'territory-audit.json'), 'utf8'),
);
if (audit.identity.read_only !== 'on')
throw new Error('Read-only audit required');
const records = audit.records.map((item) => reconcile(item, audit));
const acceptancePath = path.join(
root,
'storage/app/private/geo-ine-review-20260928-v2',
);
const acceptanceFile = fs
.readdirSync(acceptancePath)
.find(
(name) =>
name.startsWith('owner-acceptance-') && name.endsWith('.json'),
);
const acceptance = JSON.parse(
fs.readFileSync(path.join(acceptancePath, acceptanceFile), 'utf8'),
);
const crypto = require('node:crypto');
const archiveHash = crypto
.createHash('sha256')
.update(fs.readFileSync(path.join(acceptancePath, 'original.pdf')))
.digest('hex');
if (archiveHash !== acceptance.source_checksums['original.pdf'])
throw new Error(
'Accepted INE archive hash mismatch; do not reuse acceptance',
);
const clean = (s) =>
String(s ?? '')
.replaceAll('|', '\\|')
.replaceAll('\n', ' ');
const lines = [
'# Reconciliação das fontes e validações territoriais',
'',
'Critério do responsável: documentos oficiais são a única fonte de validação oficial. Programação, promotores, testes e leitura do cadastro não substituem esses documentos. Ausência de um campo de fonte/data é uma lacuna de rastreabilidade, não uma decisão de invalidade nem revogação.',
'',
'Esta revisão conserva os estados existentes. Uma referência oficial identificada não significa que todas as afirmações da ficha estejam cobertas: o âmbito da citação deve ser conservado. Não foram concedidas novas validações nem ativados cálculos. A declaração do responsável sobre a origem oficial dos dados em uso é registada; as pontes técnicas ainda incompletas são identificadas abaixo, sem contradizer essa declaração.',
'',
'## Evidência oficial e aceitação já existentes',
'',
`INE — Codificador da Divisão Político-Administrativa: exemplar privado com SHA-256 \`${archiveHash}\`, coincidente com a aceitação de ${acceptance.decision_date}. Decisão preservada: \`${acceptance.validation}\` e \`${acceptance.authenticity}\`, por instrução explícita do responsável. A aceitação não é reaberta. O próprio registo distingue-a da correspondência técnica: \`${acceptance.technical_mapping_status}\`; zero territórios importados por essa decisão. Não se atribui automaticamente esse PDF aos 1 921 nós oriundos do workbook.`,
'',
'As seis fontes de diplomas existentes conservam `validation_status=pending` tal como já estava na base; esse estado não foi imposto por esta entrega. As 14 fichas de zonas com `validated` conservam esse estado, distinguindo validação do cadastro de validação jurídica do diploma associado. Referências de promotores não passam a fonte oficial por a ficha estar validada.',
'',
'## Matriz por registo',
'',
'| Registo | Validação já registada, preservada | Fonte/citação recuperada | Lacuna específica de rastreabilidade |',
'| --- | --- | --- | --- |',
];
for (const r of records) {
const sources = [
...(r.legal_source
? [
`${r.legal_source.diploma}; ${r.legal_source.title}; ${r.legal_source.article}; ${r.legal_source.url}; versão ${r.legal_source.version_no}`,
]
: []),
...r.official_references.map(
(s) => `${s.title}; ${s.url}; âmbito: ${s.supports}`,
),
...r.non_official_references.map(
(s) =>
`Referência ${s.source_type}, não oficial: ${s.title}; ${s.url}`,
),
...r.documentary_relations.map(
(s) =>
`${s.source_reference}; citação: ${s.source_quote}; dossier ${s.dossier_id}; auditoria ${s.applied_at || 'não identificada'}`,
),
...r.dossiers.map(
(d) => `Dossier ${d.id}, versão ${d.version}: ${d.source}`,
),
];
const state = `Cadastro: ${r.existing_record_validation ?? 'campo de validação não existente/não registado nesta entidade'}; fonte legal: ${r.existing_legal_validation ?? 'sem fonte legal ligada'}`;
const gap =
r.traceability === 'official_reference_identified'
? 'Referência oficial recuperada; ligar exemplar/citação ao atributo quando essa ponte não consta do cadastro. Não repetir validação já evidenciada; não alargar o âmbito da fonte.'
: r.traceability ===
'documentary_bridge_to_official_source_to_recover'
? 'Citação e histórico documentais recuperados; recuperar a ponte ao documento oficial utilizado. Não desvalidar a relação aplicada.'
: 'Ligação ao exemplar oficial não identificada nos registos revistos; recuperar proveniência da importação. Ausência de ligação não prova ausência de fonte oficial.';
lines.push(
`| ${clean(r.reference)} | ${clean(state)} | ${clean(sources.join('; ') || 'Sem ponte documental no cadastro revisto')} | ${clean(gap)} |`,
);
}
lines.push(
'',
'## Estados independentes e limites',
'',
'Consulta autorizada permanece independente da completude da referência. Validação oficial deriva apenas da evidência documental oficial no âmbito aceite; o estado cadastral é preservado e apresentado separadamente. Utilização em regras/cálculos permanece inalterada. Não se transforma a data de criação, data de aplicação ou início de vigência em data da fonte.',
'',
'O rótulo histórico de programação v4 foi preservado; a revisão anterior encontrou programação v5 corroborante, sem o exemplar v4. Esta diferença de rótulo é uma questão de proveniência, não uma revogação das 28 relações aplicadas. Programação não substitui o documento oficial na validação oficial.',
'',
'Nenhuma base ativa foi escrita. A matriz integral privada está em `territory-source-reconciliation.json`. Diferença de cobertura do inventário, questões jurídicas realmente não resolvidas, ClamD real e primeiro pedido real continuam separados.',
);
fs.writeFileSync(
path.join(privateDir, 'territory-source-reconciliation.json'),
JSON.stringify(
{
captured_at: audit.captured_at,
policy: 'official_documents_only_for_official_validation',
owner_origin_declaration_preserved: true,
ine_acceptance: acceptance,
records,
},
null,
2,
),
);
fs.writeFileSync(
path.join(
root,
'docs/investmoz/v2/reconciliacao-fontes-territoriais.md',
),
lines.join('\n') + '\n',
);
console.log(
JSON.stringify({
records: records.length,
official_references_identified: records.filter(
(r) => r.traceability === 'official_reference_identified',
).length,
record_validations_preserved: records.filter(
(r) => r.existing_record_validation === 'validated',
).length,
accepted_ine_hash_matches: true,
active_writes: 0,
}),
);
}
@@ -0,0 +1,93 @@
const { test } = require('node:test');
const assert = require('node:assert/strict');
const { reconcile } = require('./investmoz-v2-source-reconciliation.cjs');
test('validated official source and record survive missing citation fields', () => {
const result = reconcile(
{
kind: 'investment_zones',
record: {
id: 'z',
validation_status: 'validated',
legal_source_id: 'l',
},
},
{
dossiers: [],
legal_sources: [
{
id: 'l',
validation_status: 'validated',
diploma: 'QA official fixture',
},
],
},
);
assert.equal(result.existing_record_validation, 'validated');
assert.equal(result.existing_legal_validation, 'validated');
assert.equal(result.validation_revoked, false);
assert.equal(result.traceability, 'official_reference_identified');
});
test('missing source field is a traceability gap, not rejection', () => {
const result = reconcile(
{
kind: 'projects',
record: {
id: 'p',
metadata: JSON.stringify({
documented_location_sources: {
'x:primary': {
source_quote: 'QA quotation',
source_reference: 'QA dossier',
applied_at: '2026-10-07',
},
},
}),
},
},
{ dossiers: [], legal_sources: [] },
);
assert.equal(
result.traceability,
'documentary_bridge_to_official_source_to_recover',
);
assert.equal(result.documentary_relations[0].source_quote, 'QA quotation');
assert.equal(result.validation_revoked, false);
assert.equal(result.calculation_activated, false);
});
test('promoter cannot become official through validated record', () => {
const result = reconcile(
{
kind: 'investment_zones',
record: {
id: 'z',
validation_status: 'validated',
source_evidence: JSON.stringify([
{ source_type: 'promoter', title: 'QA promoter' },
]),
},
},
{ dossiers: [], legal_sources: [] },
);
assert.equal(result.existing_record_validation, 'validated');
assert.equal(result.official_references.length, 0);
assert.equal(result.official_validation_newly_granted, false);
});
test('existing legal pending is preserved without invalidating consultation', () => {
const result = reconcile(
{
kind: 'investment_zones',
record: {
id: 'z',
validation_status: 'validated',
legal_source_id: 'l',
},
},
{
dossiers: [],
legal_sources: [{ id: 'l', validation_status: 'pending' }],
},
);
assert.equal(result.existing_record_validation, 'validated');
assert.equal(result.existing_legal_validation, 'pending');
assert.equal(result.validation_revoked, false);
});
+59
View File
@@ -0,0 +1,59 @@
<?php
use Illuminate\Foundation\Bootstrap\LoadConfiguration;
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
// Read-only audit: no Laravel kernel, migrations, seeders or data updates.
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
(new LoadEnvironmentVariables)->bootstrap($app);
(new LoadConfiguration)->bootstrap($app);
$c = $app->make('config')->get('database.connections.'.$app->make('config')->get('database.default'));
if (($c['driver'] ?? '') !== 'pgsql' || ! in_array($c['url'] ?? null, [null, ''], true)) {
throw new RuntimeException('Unsupported target');
}
$pdo = new PDO('pgsql:host='.$c['host'].';port='.$c['port'].';dbname='.$c['database'], $c['username'], $c['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$pdo->exec('BEGIN TRANSACTION READ ONLY');
$proof = ['captured_at' => gmdate('c'), 'identity' => $pdo->query("SELECT current_database() AS database, inet_server_port() AS port, current_setting('transaction_read_only') AS read_only")->fetch(PDO::FETCH_ASSOC), 'records' => []];
foreach (['opportunities', 'projects', 'economic_clusters', 'investment_zones'] as $table) {
$rows = $pdo->query('SELECT * FROM '.$table.' WHERE deleted_at IS NULL ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
foreach ($rows as $row) {
$relations = [];
$queries = match ($table) {
'opportunities' => ['own' => 'SELECT geo_location_id FROM opportunity_locations WHERE opportunity_id=?', 'projects' => 'SELECT project_id FROM opportunity_projects WHERE opportunity_id=?', 'clusters' => 'SELECT cluster_id FROM opportunity_clusters WHERE opportunity_id=?'],
'projects' => ['own' => 'SELECT * FROM project_locations WHERE project_id=? AND deleted_at IS NULL', 'clusters' => 'SELECT * FROM cluster_projects WHERE project_id=? AND deleted_at IS NULL'],
'economic_clusters' => ['members' => 'SELECT * FROM cluster_projects WHERE cluster_id=? AND deleted_at IS NULL'],
'investment_zones' => ['own' => 'SELECT * FROM investment_zone_locations WHERE zone_id=?', 'links' => 'SELECT * FROM investment_zone_links WHERE zone_id=?'],
};
foreach ($queries as $kind => $sql) {
$statement = $pdo->prepare($sql);
$statement->execute([$row['id']]);
$relations[$kind] = $statement->fetchAll(PDO::FETCH_ASSOC);
}
$issues = [];
$own = count($relations['own'] ?? []) + (empty($row['primary_territory_id']) ? 0 : 1);
if ($table === 'opportunities') {
if (! $own) {
$issues[] = ($relations['projects'] || $relations['clusters']) ? 'context_only_not_implantation' : 'insufficient_territorial_information';
}
if ($own > 1) {
$issues[] = 'multiple_links_review_distinct_ids';
}
$issues[] = 'direct_link_source_and_date_not_stored_in_pivot';
}
$proof['records'][] = ['kind' => $table, 'id' => $row['id'], 'record' => $row, 'relations' => $relations, 'findings' => $issues];
}
}
$proof['geography'] = $pdo->query('SELECT g.id,g.name,g.location_type,g.parent_id,g.source_system,g.data_classification,
p.location_type AS parent_type,p.deleted_at AS parent_deleted_at,
(SELECT count(*) FROM geo_geometries s WHERE s.geo_location_id=g.id AND s.deleted_at IS NULL) AS geometry_count
FROM geo_locations g LEFT JOIN geo_locations p ON p.id=g.parent_id WHERE g.deleted_at IS NULL ORDER BY g.id')->fetchAll(PDO::FETCH_ASSOC);
$proof['geometries'] = $pdo->query('SELECT id,geo_location_id,geometry_type,source_system,properties,data_classification,ST_IsValid(geom) AS valid,ST_IsEmpty(geom) AS empty,ST_SRID(geom) AS srid,ST_AsGeoJSON(geom) AS geometry FROM geo_geometries WHERE deleted_at IS NULL ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
$proof['dossiers'] = $pdo->query('SELECT id,project_id,cluster_id,title,source,content,version_no,publication_status FROM investment_dossiers WHERE deleted_at IS NULL ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
$proof['legal_sources'] = $pdo->query('SELECT id,title,url,diploma,article,competent_entity,validation_status,validated_at,version_no,valid_from,valid_until FROM investment_legal_sources ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
$proof['geo_sources'] = $pdo->query('SELECT id,name,responsible_entity,reference,checksum,source_version,status,approval_reference FROM geo_sources ORDER BY id')->fetchAll(PDO::FETCH_ASSOC);
$proof['geo_profiles'] = $pdo->query('SELECT geo_location_id,source_id,status,valid_from,valid_to FROM geo_territory_profiles ORDER BY geo_location_id')->fetchAll(PDO::FETCH_ASSOC);
$proof['traceability_events'] = $pdo->query("SELECT id,action,occurred_at FROM audit_events WHERE source_system='territory-traceability-v2' ORDER BY occurred_at,id")->fetchAll(PDO::FETCH_ASSOC);
$pdo->exec('ROLLBACK');
file_put_contents(dirname(__DIR__).'/storage/app/private/investmoz-v2/territory-audit.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode(['identity' => $proof['identity'], 'records' => count($proof['records']), 'territories' => count($proof['geography']), 'geometries' => count($proof['geometries'])], JSON_THROW_ON_ERROR).PHP_EOL;
+239
View File
@@ -0,0 +1,239 @@
const {
chromium,
} = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const privateDir = path.resolve(
__dirname,
'../storage/app/private/investmoz-v2',
);
const fixture = JSON.parse(
fs.readFileSync(
path.join(privateDir, 'territory-qa-fixtures.json'),
'utf8',
),
);
assert.equal(fixture.database, 'investmoz_v2_20261008_test');
assert.equal(fixture.port, 55441);
(async () => {
const browser = await chromium.launch({
headless: true,
executablePath:
'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe',
});
const checks = [];
try {
for (const width of [1440, 390]) {
const context = await browser.newContext({
viewport: { width, height: 850 },
});
await context.route('**/*', (route) =>
route.request().url().startsWith('http://127.0.0.1:18092')
? route.continue()
: route.abort(),
);
const page = await context.newPage();
const errors = [];
const writes = [];
page.on('pageerror', (e) => errors.push(e.message));
page.on('request', (r) => {
if (!['GET', 'HEAD'].includes(r.method()))
writes.push(r.method() + ' ' + r.url());
});
for (const [kind, record] of Object.entries(fixture.cases)) {
const response = await page.goto(
'http://127.0.0.1:18092/oportunidades/' + record.slug,
);
assert.equal(response.status(), 200);
assert.match(
response.headers()['x-investmoz-qa-target'] || '',
/investmoz_v2_20261008_test/,
);
try {
await page
.getByRole('heading', {
name: 'Implantação declarada da oportunidade',
})
.waitFor({ timeout: 10000 });
} catch (error) {
fs.writeFileSync(
path.join(
privateDir,
'territory-browser-diagnostic.json',
),
JSON.stringify(
{
url: page.url(),
errors,
body: await page.locator('body').innerText(),
},
null,
2,
),
);
throw error;
}
const map = await page.request.get(
'http://127.0.0.1:18092/api/v1/investment/map?opportunity_id=' +
record.id,
);
const data = await map.json();
const expected =
kind === 'multiple'
? 2
: ['direct', 'context'].includes(kind)
? 1
: 0;
assert.equal(data.features.length, expected);
const section = page
.locator('section')
.filter({
has: page.getByRole('heading', {
name: 'Implantação declarada da oportunidade',
exact: true,
}),
})
.first();
if (['context', 'insufficient'].includes(kind))
assert.match(await section.innerText(), /não documentada/);
if (kind === 'context') {
assert.equal(
data.features[0].properties.location_basis,
'linked_project_site',
);
await page
.getByRole('link', {
name: 'Consultar dossier: Territory QA dossier',
})
.click();
await page.waitForURL('**/carteira/' + fixture.dossier);
await page
.getByRole('link', {
name: record.title || 'Territory QA context',
exact: true,
})
.first()
.click();
await page.waitForURL(
'**/oportunidades/' + record.slug + '**',
);
}
if (kind === 'multiple') {
const buttons = page.getByRole('button', {
name: 'Territory QA multiple',
exact: true,
});
await buttons.first().waitFor();
assert.equal(await buttons.count(), 2);
await buttons.nth(0).click();
await buttons.nth(1).click();
assert.equal(
await buttons.nth(1).getAttribute('aria-pressed'),
'true',
);
assert.equal(
await buttons.nth(0).getAttribute('aria-pressed'),
'false',
);
}
if (kind === 'direct') {
assert.equal(data.areas.length, 1);
await page
.getByText(
/Polígono cadastrado, limites oficiais por verificar/,
)
.first()
.waitFor();
}
if (kind === 'without-point') {
await section
.getByText('Coordenadas não documentadas.')
.waitFor();
await section
.getByRole('link', {
name: 'Pesquisar oportunidades neste território',
})
.click();
await page.waitForURL(
'**/oportunidades?geo_location_id=' + fixture.district,
);
await page
.getByRole('link', {
name: 'Territory QA without-point',
exact: true,
})
.first()
.waitFor();
}
checks.push({
width,
kind,
markers: data.features.length,
areas: data.areas.length,
passed: true,
});
}
await page.goto('http://127.0.0.1:18092/oportunidades');
await page
.getByLabel('Base da relação territorial')
.selectOption('context');
await page
.getByRole('button', { name: 'Aplicar filtros', exact: true })
.click();
await page.waitForURL('**territory_basis=context**');
await page
.getByRole('link', {
name: 'Territory QA context',
exact: true,
})
.first()
.waitFor();
assert.equal(
await page
.getByRole('link', {
name: 'Territory QA direct',
exact: true,
})
.count(),
0,
);
await page
.getByRole('status')
.filter({ hasText: 'locais com coordenadas disponíveis.' })
.first()
.waitFor();
assert.deepEqual(errors, []);
assert.deepEqual(writes, []);
await page.screenshot({
path: path.join(
privateDir,
'territory-browser-' + width + '.png',
),
fullPage: true,
});
await context.close();
}
fs.writeFileSync(
path.join(privateDir, 'territory-browser.json'),
JSON.stringify(
{
database: fixture.database,
port: fixture.port,
external_requests_blocked: true,
checks,
},
null,
2,
),
);
console.log(
JSON.stringify({ passed: true, checks: checks.length, writes: 0 }),
);
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
@@ -0,0 +1,66 @@
<?php
use App\Models\InvestmentDossier;
use App\Models\Opportunity;
use App\Models\Project;
use App\Models\Sector;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441', 'DB_DATABASE' => 'investmoz_v2_20261008_test', 'DB_USERNAME' => 'postgres', 'DB_PASSWORD' => '', 'DB_URL' => ''] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
require dirname(__DIR__).'/vendor/autoload.php';
$app = require dirname(__DIR__).'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$connection = DB::connection();
if ($connection->getDatabaseName() !== 'investmoz_v2_20261008_test' || (string) $connection->getConfig('port') !== '55441' || $connection->getConfig('host') !== '127.0.0.1' || ! in_array($connection->getConfig('url'), [null, ''], true)) {
throw new RuntimeException('QA configuration required');
}
$identity = DB::selectOne('SELECT current_database() AS name,inet_server_port() AS port');
if ($identity->name !== 'investmoz_v2_20261008_test' || (int) $identity->port !== 55441) {
throw new RuntimeException('QA identity required');
}
$manifest = DB::transaction(function () {
$nodes = [];
$parent = null;
foreach (['country', 'province', 'district', 'admin_post', 'locality', 'site'] as $type) {
$id = (string) Str::uuid();
DB::table('geo_locations')->insert(['id' => $id, 'code' => $id, 'name' => 'Territory QA '.$type, 'location_type' => $type, 'parent_id' => $parent, 'data_classification' => 'public',
'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]);
$nodes[$type] = $id;
$parent = $id;
}
$second = (string) Str::uuid();
DB::table('geo_locations')->insert(['id' => $second, 'code' => $second, 'name' => 'Territory QA second site', 'location_type' => 'site', 'parent_id' => $nodes['locality'], 'data_classification' => 'public']);
foreach ([$nodes['site'], $second] as $index => $id) {
DB::table('geo_geometries')->insert(['id' => (string) Str::uuid(), 'geo_location_id' => $id, 'geometry_type' => 'point', 'data_classification' => 'public', 'source_system' => 'Synthetic QA coordinates; no real source',
'geom' => DB::raw('ST_SetSRID(ST_MakePoint('.(32.5 + $index / 10).',-25.9),4326)'), 'properties' => json_encode(['source_date' => '2026-10-08'])]);
}
DB::table('geo_geometries')->insert(['id' => (string) Str::uuid(), 'geo_location_id' => $nodes['site'], 'geometry_type' => 'polygon', 'data_classification' => 'public', 'source_system' => 'Synthetic QA boundary; not official',
'geom' => DB::raw("ST_GeomFromText('POLYGON((32 -26,33 -26,33 -25,32 -25,32 -26))',4326)"), 'properties' => json_encode(['source_date' => '2026-10-08'])]);
$project = Project::factory()->create(['name' => 'Territory QA project', 'status' => 'draft', 'data_classification' => 'internal']);
$dossier = InvestmentDossier::create(['project_id' => $project->id, 'title' => 'Territory QA dossier', 'source' => 'Synthetic QA source', 'content' => ['location' => 'Synthetic QA site'], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'approval_basis' => 'document_owner', 'approval_evidence' => 'Synthetic isolated QA disclosure']);
DB::table('project_locations')->insert(['id' => (string) Str::uuid(), 'project_id' => $project->id, 'geo_location_id' => $nodes['site'], 'role' => 'primary', 'data_classification' => 'public', 'source_system' => 'Synthetic QA source']);
DB::table('project_locations')->insert(['id' => (string) Str::uuid(), 'project_id' => $project->id, 'geo_location_id' => $nodes['district'], 'role' => 'impact', 'data_classification' => 'public', 'source_system' => 'Synthetic QA impact']);
$sector = Sector::firstOrCreate(['slug' => 'territory-qa'], ['name' => 'Territory QA', 'is_active' => true]);
$cases = [];
foreach (['direct', 'context', 'multiple', 'insufficient', 'without-point'] as $kind) {
$opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'TERR-QA-'.Str::uuid(), 'title' => 'Territory QA '.$kind, 'slug' => 'territory-qa-'.$kind.'-'.Str::uuid(), 'status' => 'published', 'published_at' => now()->subMinute()]);
if ($kind === 'context') {
DB::table('opportunity_projects')->insert(['opportunity_id' => $opportunity->id, 'project_id' => $project->id]);
}
foreach (match ($kind) {
'direct' => [$nodes['site']], 'multiple' => [$nodes['site'], $second], 'without-point' => [$nodes['district']], default => []
} as $id) {
DB::table('opportunity_locations')->insert(['opportunity_id' => $opportunity->id, 'geo_location_id' => $id]);
}
$cases[$kind] = ['id' => $opportunity->id, 'slug' => $opportunity->slug];
}
return ['database' => 'investmoz_v2_20261008_test', 'port' => 55441, 'cases' => $cases, 'dossier' => $dossier->id, 'district' => $nodes['district']];
});
file_put_contents(dirname(__DIR__).'/storage/app/private/investmoz-v2/territory-qa-fixtures.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode($manifest, JSON_THROW_ON_ERROR).PHP_EOL;
+216
View File
@@ -0,0 +1,216 @@
const fs = require('node:fs');
const path = require('node:path');
const { reconcile } = require('./investmoz-v2-source-reconciliation.cjs');
const root = path.resolve(__dirname, '..');
const audit = JSON.parse(
fs.readFileSync(
path.join(
root,
'storage/app/private/investmoz-v2/territory-audit.json',
),
'utf8',
),
);
if (audit.identity.read_only !== 'on')
throw new Error('Read-only evidence required');
const geo = new Map(audit.geography.map((g) => [g.id, g]));
const clean = (value) =>
String(value ?? '')
.replaceAll('|', '\\|')
.replaceAll('\n', ' ');
const lineage = (id) => {
const visited = new Set();
const nodes = [];
const issues = [];
while (id) {
if (visited.has(id)) {
issues.push('ciclo');
break;
}
visited.add(id);
const g = geo.get(id);
if (!g) {
issues.push('nó/antepassado ausente');
break;
}
nodes.unshift(g);
id = g.parent_id;
}
const ranks = [
'country',
'province',
'district',
'admin_post',
'locality',
'site',
];
for (let i = 1; i < nodes.length; i++) {
if (
nodes[i].location_type !== 'site' &&
ranks.indexOf(nodes[i].location_type) !==
ranks.indexOf(nodes[i - 1].location_type) + 1
)
issues.push('hierarquia incompatível');
}
return { names: nodes.map((g) => g.name).join(' → '), issues };
};
const lines = [
'# Qualidade territorial e relações — entrega V2',
'',
`Leitura protegida em ${audit.captured_at}, base ativa ${audit.identity.database}:${audit.identity.port}, transação READ ONLY terminada por ROLLBACK. Nenhum dado ativo alterado. Evidência completa privada: \`territory-audit.json\`.`,
'',
'## Resultados por registo e propostas de dados',
'',
'Cada linha identifica o estado cadastrado, a fonte consultada, motivo e impacto da proposta. Documentos oficiais são a única fonte de validação oficial. Ausência de campos de fonte/data é uma lacuna de rastreabilidade, não invalidade ou revogação. Estados e aceitações existentes são preservados na [matriz de reconciliação](reconciliacao-fontes-territoriais.md). Sem associação explícita não se cria uma relação por nome semelhante, cluster ou proximidade.',
'',
'| Registo | Relações/localizações cadastradas | Fonte consultada | Proposta, motivo e impacto |',
'| --- | --- | --- | --- |',
];
const result = [];
for (const item of audit.records) {
const r = item.record;
const reconciled = reconcile(item, audit);
const metadata = JSON.parse(r.metadata || '{}');
const own = (item.relations.own || []).map((l) => ({
id: l.geo_location_id,
role: l.role || l.scope_type || 'declarada',
source:
metadata.documented_location_sources?.[
l.geo_location_id + ':' + l.role
]?.source_reference ||
l.source_reference ||
l.source_system ||
null,
}));
if (
r.primary_territory_id &&
!own.some((l) => l.id === r.primary_territory_id)
)
own.unshift({
id: r.primary_territory_id,
role: 'principal cadastrada',
source: r.source_system || null,
});
for (const c of metadata.documented_coverage || [])
own.push({
id: c.id,
role: 'abrangência',
source: c.source_reference || null,
});
const locations = own.map((l) => ({ ...l, ...lineage(l.id) }));
const dossiers = audit.dossiers.filter(
(d) => d.project_id === r.id || d.cluster_id === r.id,
);
const source =
[
...new Set(
[
r.source_system,
...dossiers.map((d) => d.source),
...locations.map((l) => l.source),
].filter(Boolean),
),
].join('; ') ||
'Cadastro da oportunidade; fonte da relação e data ausentes';
const proposed = [];
if (item.kind === 'opportunities')
proposed.push(
'Recuperar a ligação ao documento oficial utilizado e a data/citação no cadastro, sem desvalidar o dado; associações a projetos/clusters ausentes: recuperar a relação documentada, sem criar vínculos automaticamente',
);
if (!own.length)
proposed.push(
'Sem localização estruturada: preservar descrição e obter identificação documental; não estimar ponto',
);
if (locations.some((l) => l.issues.length))
proposed.push(
'Rever hierarquia indicada na evidência; não mudar parent_id automaticamente',
);
if (own.length > 1)
proposed.push(
'Preservar locais/papéis distintos; não selecionar um único local nem calcular centroide',
);
if (item.kind === 'economic_clusters')
proposed.push(
'Abrangência do cluster distinta dos locais dos membros; não deduzir perímetro',
);
if (item.kind === 'investment_zones')
proposed.push(
'Preservar a validação registada e ligar fonte/citação ao atributo no respetivo âmbito; localização/regime não prova outros atributos como disponibilidade ou capacidade',
);
if (!proposed.length)
proposed.push(
'Preservar relação documental e validações existentes; completar a rastreabilidade da evidência/data quando ausente no cadastro',
);
proposed.push(
'Coordenadas/polígonos não cadastrados nesta leitura; ficha continua consultável',
);
const label = r.code || r.reference || String(r.id);
const relations =
locations
.map(
(l) =>
`${l.role}: ${l.names || l.id}${l.issues.length ? ' [' + l.issues.join(', ') + ']' : ''}`,
)
.join('; ') || 'Sem localização estruturada';
const counts = `projetos: ${(item.relations.projects || []).length}; clusters: ${(item.relations.clusters || []).length}; membros: ${(item.relations.members || []).length}; ligações de zona: ${(item.relations.links || []).length}`;
lines.push(
`| ${clean(label)} | ${clean(relations + '; ' + counts)} | ${clean(source + '; cadastro: ' + (reconciled.existing_record_validation ?? 'sem campo/estado registado') + '; fonte legal: ' + (reconciled.existing_legal_validation ?? 'sem ligação no cadastro'))} | ${clean(proposed.join('; '))} |`,
);
result.push({
kind: item.kind,
id: r.id,
reference: label,
locations,
proposals: proposed,
source_reconciliation: reconciled,
});
}
const hierarchy = audit.geography
.map((g) => ({ id: g.id, ...lineage(g.id) }))
.filter((g) => g.issues.length);
lines.push(
'',
'## Hierarquia e cartografia',
'',
`${audit.geography.length} nós administrativos/locais percorridos; ${hierarchy.length} caminhos com inconsistência estrutural identificada por esta verificação. Ausência de inconsistência cadastral não certifica fronteiras oficiais. ${audit.geometries.length} geometrias ativas cadastradas: nenhum ponto, polígono ou centroide gerado.`,
'',
'## Correções de código e QA',
'',
'- Ficha apresenta implantação declarada separada dos locais/impacto dos projetos e abrangência dos clusters; fonte/data ausentes explícitas.',
'- Filtros distinguem implantação declarada de apenas contexto associado; localizações sem coordenadas continuam pesquisáveis.',
'- Mapa mantém vários locais do mesmo investimento e seleção independente; áreas cadastradas são apresentadas com proveniência, sem homologar limites.',
'- Associação existente continua a permitir navegação oportunidade → dossier → oportunidade e território → pesquisa. Permissões e relações revogadas preservadas.',
'',
'Ver resultados de QA no backlog e na matriz de reconciliação. Propostas desta tabela não são alterações aplicadas nem plano de escrita aprovado; não revogam validações documentais existentes.',
'',
'## Pendências independentes',
'',
'Diferença de cobertura do inventário documental, validação jurídica/documental das fontes, ClamD real e primeiro pedido real no Balcão permanecem separados. Nenhum teste territorial conclui essas pendências. Aplicação futura de dados exige backup/restauro confirmado e ação autenticada existente.',
);
fs.writeFileSync(
path.join(root, 'docs/investmoz/v2/qualidade-territorial.md'),
lines.join('\n') + '\n',
);
fs.writeFileSync(
path.join(
root,
'storage/app/private/investmoz-v2/territory-proposals.json',
),
JSON.stringify(
{
captured_at: audit.captured_at,
applied: false,
records: result,
hierarchy,
},
null,
2,
),
);
console.log(
JSON.stringify({
records: result.length,
hierarchy_issues: hierarchy.length,
geometry_count: audit.geometries.length,
}),
);
+7
View File
@@ -0,0 +1,7 @@
<?php
// Reuse the existing read-only active backup and isolated restore procedure.
require __DIR__.'/desk-activation-backup.php';
$proof = json_decode(file_get_contents(dirname(__DIR__).'/storage/app/private/virtual-desk/backup-proof.json'), true, 512, JSON_THROW_ON_ERROR);
$proof['reversal_verified'] = false;
file_put_contents(dirname(__DIR__).'/storage/app/private/investmoz-v2/territory-backup-proof.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
+79
View File
@@ -0,0 +1,79 @@
<?php
use App\Services\InvestmentTerritorialTraceability;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\DB;
// Only the restored, isolated backup can be mutated by this compatibility rehearsal.
$root = dirname(__DIR__);
$directory = $root.'/storage/app/private/investmoz-v2';
$proofFile = $directory.'/territory-backup-proof.json';
$proof = json_decode(file_get_contents($proofFile), true, 512, JSON_THROW_ON_ERROR);
$target = $proof['restored_target'];
if (! preg_match('/^desk_restore_\d{8}_\d{6}_test$/', $target['database']) || (int) $target['port'] !== 55441 || ! $proof['restore_verified']) {
throw new RuntimeException('Verified isolated restoration required.');
}
foreach (['APP_ENV' => 'testing', 'APP_KEY' => 'base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441', 'DB_DATABASE' => $target['database'], 'DB_USERNAME' => 'postgres', 'DB_PASSWORD' => '', 'DB_URL' => '', 'CACHE_STORE' => 'array', 'QUEUE_CONNECTION' => 'sync'] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
require $root.'/vendor/autoload.php';
$app = require $root.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$connection = config('database.connections.pgsql');
if (config('database.default') !== 'pgsql' || $connection['host'] !== '127.0.0.1' || (int) $connection['port'] !== 55441 || $connection['database'] !== $target['database'] || ! empty($connection['url'])) {
throw new RuntimeException('Refusing non-QA configuration.');
}
$identity = DB::selectOne('SELECT current_database() AS database, inet_server_port() AS port');
if ($identity->database !== $target['database'] || (int) $identity->port !== 55441) {
throw new RuntimeException('Refusing non-QA connection.');
}
$qaDirectory = $directory.'/qa-traceability-reversal';
if (! is_dir($qaDirectory)) {
mkdir($qaDirectory, 0700, true);
}
$activeBytes = file_get_contents($directory.'/territory-activation-plan.json');
$activePlan = json_decode($activeBytes, true, 512, JSON_THROW_ON_ERROR);
$plan = $activePlan;
$plan['target'] = $target;
$qaBytes = json_encode($plan, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
$qaHash = hash('sha256', $qaBytes);
file_put_contents($qaDirectory.'/territory-activation-plan.json', $qaBytes);
// This fixture authorizes the QA rehearsal only; it is not evidence of a completed reversal.
$qaProof = [...$proof, 'target' => $target, 'reversal_verified' => true, 'plan_hash' => $qaHash, 'code_hash' => $plan['code_hash'], 'fixture_only' => true];
file_put_contents($qaDirectory.'/territory-backup-proof.json', json_encode($qaProof, JSON_THROW_ON_ERROR));
config(['investment.traceability_directory' => $qaDirectory]);
$service = app(InvestmentTerritorialTraceability::class);
$snapshot = fn () => DB::table('cluster_projects')->orderBy('id')->get(['id', 'source_reference', 'evidence_reference', 'validation_status', 'association_type', 'version_no'])->map(fn ($row) => (array) $row)->all();
$before = $snapshot();
$unchangedTables = ['projects', 'opportunities', 'economic_clusters', 'investment_zones', 'investment_legal_sources', 'investment_dossiers', 'project_locations', 'opportunity_projects', 'opportunity_clusters', 'geo_locations', 'geo_geometries'];
$hashes = fn () => array_combine($unchangedTables, array_map(fn ($table) => hash('sha256', json_encode(DB::table($table)->orderByRaw('to_jsonb('.$table.')::text')->get(), JSON_THROW_ON_ERROR)), $unchangedTables));
$beforeHashes = $hashes();
$actor = DB::table('users')->insertGetId(['name' => 'Territory reversal QA', 'email' => 'territory-reversal-'.bin2hex(random_bytes(5)).'@example.invalid', 'password' => password_hash('QA-only', PASSWORD_BCRYPT), 'created_at' => now(), 'updated_at' => now()]);
$applied = $service->apply($qaHash, $actor);
$replayed = $service->apply($qaHash, $actor);
if ($applied['changed'] !== count($plan['updates']) || ! $replayed['replayed'] || $replayed['changed'] !== 0) {
throw new RuntimeException('Application/idempotence failed.');
}
$sources = $service->sources('economic_clusters', $plan['rows'][array_search('economic_clusters', array_column($plan['rows'], 'kind'))]['record_id']);
$reversed = $service->apply($qaHash, $actor, true);
$after = $snapshot();
foreach ($after as $index => $row) {
$expected = $before[$index];
$changed = in_array($row['id'], array_column($plan['updates'], 'id'), true);
$expected['version_no'] += $changed ? 2 : 0;
if ($row !== $expected) {
throw new RuntimeException('Reversal did not preserve values, validation or monotonic version.');
}
}
if ($hashes() !== $beforeHashes || $reversed['changed'] !== count($plan['updates']) || ! $sources) {
throw new RuntimeException('Unrelated data changed or sources not presented.');
}
$evidence = ['verified_at' => gmdate('c'), 'target' => $target, 'plan_hash' => hash('sha256', $activeBytes), 'code_hash' => $plan['code_hash'], 'applied' => $applied, 'replayed' => $replayed, 'reversed' => $reversed, 'source_projection_count' => count($sources), 'unchanged_tables' => $unchangedTables, 'validation_states_preserved' => true, 'versions_monotonic' => true, 'active_writes' => 0];
file_put_contents($directory.'/territory-reversal-evidence.json', json_encode($evidence, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
$proof['reversal_verified'] = true;
$proof['plan_hash'] = $evidence['plan_hash'];
$proof['code_hash'] = $evidence['code_hash'];
$proof['reversal_evidence'] = 'territory-reversal-evidence.json';
file_put_contents($proofFile, json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
echo json_encode($evidence, JSON_THROW_ON_ERROR).PHP_EOL;
+180
View File
@@ -0,0 +1,180 @@
const {
chromium,
} = require('C:/Users/macbook/.cache/codex-runtimes/codex-primary-runtime/dependencies/node/node_modules/playwright');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const directory = path.resolve(
__dirname,
'../storage/app/private/investmoz-v2',
);
const plan = JSON.parse(
fs.readFileSync(
path.join(
directory,
'qa-traceability-browser/territory-activation-plan.json',
),
'utf8',
),
);
assert.match(plan.target.database, /^desk_restore_\d{8}_\d{6}_test$/);
assert.equal(plan.target.port, 55441);
const base = 'http://127.0.0.1:18093';
(async () => {
const browser = await chromium.launch({
headless: true,
executablePath:
'C:/Users/macbook/AppData/Local/ms-playwright/chromium-1243/chrome-win64/chrome.exe',
});
try {
const context = await browser.newContext({
viewport: { width: 1440, height: 950 },
});
await context.route('**/*', (route) =>
route.request().url().startsWith(base)
? route.continue()
: route.abort(),
);
const page = await context.newPage();
const errors = [];
const writes = [];
page.on('pageerror', (error) => errors.push(error.message));
page.on('request', (request) => {
if (request.method() === 'POST')
writes.push(new URL(request.url()).pathname);
});
const response = await page.goto(base + '/login');
assert.equal(
response.headers()['x-investmoz-qa-target'],
plan.target.database + ':55441',
);
await page
.locator('input[name=email]')
.fill('territory-browser@example.invalid');
await page.locator('input[name=password]').fill('Territory-QA-2026!');
await page.getByRole('button', { name: 'Log in', exact: true }).click();
await page.waitForURL((url) => url.pathname !== '/login');
await page.goto(base + '/management/relationship-repair');
const apply = page.getByRole('button', {
name: 'Aplicar rastreabilidade territorial V2',
exact: true,
});
await apply.waitFor();
assert.equal(await apply.isEnabled(), true);
assert.match(
await page.locator('body').innerText(),
/112 associações\/fontes/,
);
assert.match(
await page.locator('body').innerText(),
/13 atualizações de referências/,
);
await page.screenshot({
path: path.join(directory, 'territory-activation-preview.png'),
});
const post = page.waitForResponse(
(response) =>
response.request().method() === 'POST' &&
response.url().endsWith('/management/relationship-repair'),
);
await apply.click();
assert.equal((await post).status(), 302);
const revert = page.getByRole('button', {
name: 'Reverter este incremento de rastreabilidade',
exact: true,
});
try {
await revert.waitFor({ timeout: 8000 });
} catch (error) {
fs.writeFileSync(
path.join(directory, 'territory-browser-failure.txt'),
await page.locator('body').innerText(),
);
throw error;
}
assert.match(
await page.locator('body').innerText(),
/Rastreabilidade aplicada; validações e dados territoriais preservados/,
);
await page.reload();
await page
.getByRole('button', {
name: 'Reverter este incremento de rastreabilidade',
exact: true,
})
.waitFor();
const row = plan.rows.find(
(row) =>
row.kind === 'economic_clusters' &&
row.dossier_id &&
row.association_id,
);
await page.goto(base + '/carteira/' + row.dossier_id);
await page
.getByRole('heading', {
name: 'Fundamento documental das associações',
exact: true,
})
.waitFor();
assert.match(
await page.locator('body').innerText(),
/citação recuperada; ponte ao exemplar oficial por recuperar/,
);
await page.goto(base + '/management/relationship-repair');
const reversal = page.waitForResponse(
(response) =>
response.request().method() === 'POST' &&
response.url().endsWith('/management/relationship-repair'),
);
await page
.getByRole('button', {
name: 'Reverter este incremento de rastreabilidade',
exact: true,
})
.click();
assert.equal((await reversal).status(), 302);
await page
.getByRole('button', {
name: 'Aplicar rastreabilidade territorial V2',
exact: true,
})
.waitFor();
assert.match(
await page.locator('body').innerText(),
/Rastreabilidade revertida, histórico preservado/,
);
assert.deepEqual(errors, []);
assert.deepEqual(
writes.filter((path) => path !== '/login'),
[
'/management/relationship-repair',
'/management/relationship-repair',
],
);
const evidence = {
recorded_at: new Date().toISOString(),
target: plan.target,
checks: [
'authenticated preview',
'apply exact V2 button',
'HTTP 302 and visible success',
'recovery after reload',
'source per association on public dossier',
'revert exact increment',
'no console errors',
],
writes,
active_writes: 0,
};
fs.writeFileSync(
path.join(directory, 'territory-activation-browser.json'),
JSON.stringify(evidence, null, 2),
);
console.log(JSON.stringify(evidence));
} finally {
await browser.close();
}
})().catch((error) => {
console.error(error);
process.exitCode = 1;
});
@@ -0,0 +1,67 @@
<?php
use App\Models\User;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Vite;
$root = dirname(__DIR__);
$directory = $root.'/storage/app/private/investmoz-v2';
$proof = json_decode(file_get_contents($directory.'/territory-backup-proof.json'), true, 512, JSON_THROW_ON_ERROR);
$target = $proof['restored_target'];
if (! preg_match('/^desk_restore_\d{8}_\d{6}_test$/', $target['database']) || (int) $target['port'] !== 55441) {
throw new RuntimeException('Isolated restored QA target required.');
}
foreach (['APP_ENV' => 'testing', 'APP_DEBUG' => 'true', 'APP_URL' => 'http://127.0.0.1:18093', 'APP_KEY' => 'base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', 'DB_CONNECTION' => 'pgsql', 'DB_HOST' => '127.0.0.1', 'DB_PORT' => '55441', 'DB_DATABASE' => $target['database'], 'DB_USERNAME' => 'postgres', 'DB_PASSWORD' => '', 'DB_URL' => '', 'SESSION_DRIVER' => 'file', 'SESSION_COOKIE' => 'territory_traceability_qa', 'CACHE_STORE' => 'array', 'QUEUE_CONNECTION' => 'sync'] as $key => $value) {
putenv($key.'='.$value);
$_ENV[$key] = $_SERVER[$key] = $value;
}
require $root.'/vendor/autoload.php';
$app = require $root.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
$c = config('database.connections.pgsql');
if (config('database.default') !== 'pgsql' || $c['host'] !== '127.0.0.1' || (int) $c['port'] !== 55441 || $c['database'] !== $target['database'] || ! empty($c['url'])) {
throw new RuntimeException('Refusing non-QA configuration.');
}
$identity = DB::selectOne('SELECT current_database() AS database,inet_server_port() AS port');
if ($identity->database !== $target['database'] || (int) $identity->port !== 55441) {
throw new RuntimeException('Refusing non-QA connection.');
}
$qa = $directory.'/qa-traceability-browser';
if (! is_dir($qa.'/sessions')) {
mkdir($qa.'/sessions', 0700, true);
}
config(['session.files' => $qa.'/sessions', 'portal.managers' => ['territory-browser@example.invalid'], 'investment.traceability_directory' => $qa, 'investment.repair_directory' => $qa, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
Vite::useHotFile($root.'/storage/framework/territory-qa-absent-hot');
if (PHP_SAPI === 'cli' && ($argv[1] ?? '') === 'seed') {
$user = User::updateOrCreate(['email' => 'territory-browser@example.invalid'], ['name' => 'Territory browser QA', 'password' => Hash::make('Territory-QA-2026!'), 'email_verified_at' => now()]);
$user->forceFill(['email_verified_at' => now()])->save();
$plan = json_decode(file_get_contents($directory.'/territory-activation-plan.json'), true, 512, JSON_THROW_ON_ERROR);
$plan['target'] = $target;
$plan['prepared_at'] = gmdate('c');
foreach ($plan['guards'] as &$guard) {
if ($guard['table'] === 'cluster_projects') {
$row = DB::table('cluster_projects')->where('id', $guard['id'])->first();
foreach (array_keys($guard['expected']) as $field) {
$guard['expected'][$field] = $row->$field;
}
}
}
unset($guard);
$bytes = json_encode($plan, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
file_put_contents($qa.'/territory-activation-plan.json', $bytes);
file_put_contents($qa.'/territory-backup-proof.json', json_encode([...$proof, 'target' => $target, 'plan_hash' => hash('sha256', $bytes), 'fixture_only' => true], JSON_THROW_ON_ERROR));
file_put_contents($qa.'/preview.json', json_encode(['target' => $target, 'locations' => [], 'memberships' => [], 'pending' => [], 'prepared_opportunities' => []], JSON_THROW_ON_ERROR));
echo json_encode(['target' => $target, 'seeded_qa_user' => $user->id, 'active_writes' => 0]).PHP_EOL;
exit;
}
$path = rawurldecode(parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?: '/');
$asset = realpath($root.'/public'.$path);
$public = realpath($root.'/public');
if ($path !== '/' && $asset && $public && str_starts_with($asset, $public.DIRECTORY_SEPARATOR) && is_file($asset)) {
return false;
}
header('X-Investmoz-QA-Target: '.$identity->database.':'.$identity->port);
$app->handleRequest(Request::capture());