diff --git a/AGENTS.md b/AGENTS.md index 16ff3deb..b1e0466d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. diff --git a/app/Http/Controllers/InvestmentRelationshipRepairController.php b/app/Http/Controllers/InvestmentRelationshipRepairController.php index 891461db..f428b907 100644 --- a/app/Http/Controllers/InvestmentRelationshipRepairController.php +++ b/app/Http/Controllers/InvestmentRelationshipRepairController.php @@ -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); diff --git a/app/Http/Controllers/InvestmentZoneController.php b/app/Http/Controllers/InvestmentZoneController.php index 36532e7e..56d3ec12 100644 --- a/app/Http/Controllers/InvestmentZoneController.php +++ b/app/Http/Controllers/InvestmentZoneController.php @@ -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']) : []]; diff --git a/app/Services/InvestmentCatalogue.php b/app/Services/InvestmentCatalogue.php index 494dabda..eb1e91a3 100644 --- a/app/Services/InvestmentCatalogue.php +++ b/app/Services/InvestmentCatalogue.php @@ -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]; } } diff --git a/app/Services/InvestmentRelations.php b/app/Services/InvestmentRelations.php index 9ef35cc3..3b105620 100644 --- a/app/Services/InvestmentRelations.php +++ b/app/Services/InvestmentRelations.php @@ -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> + */ + 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 */ 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']); diff --git a/app/Services/InvestmentTerritorialTraceability.php b/app/Services/InvestmentTerritorialTraceability.php new file mode 100644 index 00000000..401de5b2 --- /dev/null +++ b/app/Services/InvestmentTerritorialTraceability.php @@ -0,0 +1,159 @@ + 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|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 $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 $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 */ + 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> */ + 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; + } +} diff --git a/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md b/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md new file mode 100644 index 00000000..927e023c --- /dev/null +++ b/docs/investmoz/v2/ativacao-rastreabilidade-territorial.md @@ -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. diff --git a/docs/investmoz/v2/backlog-mestre.md b/docs/investmoz/v2/backlog-mestre.md index e0d6297a..b8ff3a57 100644 --- a/docs/investmoz/v2/backlog-mestre.md +++ b/docs/investmoz/v2/backlog-mestre.md @@ -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. diff --git a/docs/investmoz/v2/qualidade-territorial.md b/docs/investmoz/v2/qualidade-territorial.md new file mode 100644 index 00000000..9013cb5b --- /dev/null +++ b/docs/investmoz/v2/qualidade-territorial.md @@ -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. diff --git a/docs/investmoz/v2/reconciliacao-fontes-territoriais.md b/docs/investmoz/v2/reconciliacao-fontes-territoriais.md new file mode 100644 index 00000000..fe7c2780 --- /dev/null +++ b/docs/investmoz/v2/reconciliacao-fontes-territoriais.md @@ -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. diff --git a/resources/js/components/interactive-investment-map.tsx b/resources/js/components/interactive-investment-map.tsx index 6981bdb3..6c66d82d 100644 --- a/resources/js/components/interactive-investment-map.tsx +++ b/resources/js/components/interactive-investment-map.tsx @@ -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(null); const map = useRef(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 (
A carregar localizações…

) : (

- {points.length} investimentos com coordenadas disponíveis. + {points.length} locais com coordenadas disponíveis.

)}
    - {(points ?? []).map((p) => ( + {(points ?? []).map((p, index) => (
  • { - 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.properties.category} · {p.properties.location}

    +

    + Fonte:{' '} + {p.properties.source_reference ?? 'Não indicada'} · + Data:{' '} + {p.properties.source_date ?? 'Não documentada'} +

    Abrir ficha
  • ))}
+ {areas.map((area, index) => ( +

+ + {area.title} + {' '} + · Polígono cadastrado, limites oficiais por verificar · + Fonte: {area.source_reference ?? 'Não indicada'} · Data:{' '} + {area.source_date ?? 'Não documentada'} +

+ ))}
); } diff --git a/resources/js/components/investment-map-panel.tsx b/resources/js/components/investment-map-panel.tsx index 7f6f259b..d2f212ac 100644 --- a/resources/js/components/investment-map-panel.tsx +++ b/resources/js/components/investment-map-panel.tsx @@ -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([]); 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 (
{error &&

{error}

} - + {missing.length > 0 && (

- {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:

    {missing.map((record) => ( diff --git a/resources/js/components/structure-geography.tsx b/resources/js/components/structure-geography.tsx index ccdab07a..61e41d7b 100644 --- a/resources/js/components/structure-geography.tsx +++ b/resources/js/components/structure-geography.tsx @@ -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 = { + 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({

    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'}

    ))} {!location.coordinates.length && ( @@ -98,7 +114,20 @@ export function LocationList({

    Fonte da relação:{' '} {location.source_reference ?? 'Não indicada'} + {' · Data da fonte: '} + {location.source_date ?? 'Não documentada'}

    + {location.boundaries?.map((boundary) => ( +

    + Polígono cadastrado · Fonte:{' '} + {boundary.source_reference ?? 'Não indicada'} · + Data: {boundary.source_date ?? 'Não documentada'}. O + cadastro não certifica limites oficiais. +

    + ))} + {!location.boundaries?.length && ( +

    Polígono não documentado.

    + )} + {!geography.locations.length && (

    {cluster @@ -178,7 +208,27 @@ export default function StructureGeographyPanel({ um ponto único atribuído ao cluster.

    )} - + + (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 && (

    Sem coordenadas documentadas dos locais. Nenhum ponto foi diff --git a/resources/js/components/territorial-source-evidence.tsx b/resources/js/components/territorial-source-evidence.tsx new file mode 100644 index 00000000..072f199c --- /dev/null +++ b/resources/js/components/territorial-source-evidence.tsx @@ -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 ( +

    +

    + Fundamento documental das associações +

    +

    + 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. +

    + {sources.map((source, i) => ( +
    +

    + {source.role} — {source.association} +

    +

    + Fonte:{' '} + {source.source_reference ?? + 'ligação ao exemplar por recuperar'} +

    +

    {source.source_quote ?? source.supports}

    +

    + {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. +

    +
    + ))} +
    + ); +} diff --git a/resources/js/components/territorial-traceability-plan.tsx b/resources/js/components/territorial-traceability-plan.tsx new file mode 100644 index 00000000..9466e314 --- /dev/null +++ b/resources/js/components/territorial-traceability-plan.tsx @@ -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 ( +
    +

    + V2 — aplicar rastreabilidade territorial +

    +

    + 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. +

    +

    + {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. +

    +

    + 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'} +

    +
    { + e.preventDefault(); + form.transform(() => ({ + hash: preview.hash, + operation: 'traceability', + reverse: preview.applied, + })); + form.post('/management/relationship-repair', { + preserveScroll: true, + }); + }} + > + + {Object.values(form.errors).map((error) => ( +

    + {error} +

    + ))} +
    +

    Alterações aos campos existentes

    + {preview.plan.updates.map((update) => ( +
    +

    {update.reference}

    +

    + Referência atual:{' '} + {update.current.source_reference ?? 'não preenchida'}; + evidência atual:{' '} + {update.current.evidence_reference ?? 'não preenchida'}. +

    +

    + Proposta: {update.values.source_reference}; dossier{' '} + {update.values.evidence_reference}. +

    +

    Fundamento: {update.source_quote}

    +

    {update.effect}

    +
    + ))} +

    Fundamento por associação

    + {preview.plan.rows.map((row) => ( +
    +

    + {row.reference} — {row.association} ({row.role}) +

    +

    + Atual:{' '} + {row.existing_validation ?? + 'sem campo de validação nesta associação'} + ; estado preservado. +

    +

    + Proposta: ligar a referência e o fundamento. Fonte:{' '} + {row.source_reference ?? 'por recuperar no acervo'} +

    +

    Citação/âmbito: {row.source_quote ?? row.supports}

    +

    + Rastreabilidade: {row.traceability} · Efeito:{' '} + {row.effect} +

    +
    + ))} +

    + Pendências de rastreabilidade (não invalidam dados) +

    + {preview.plan.pending.map((row, i) => ( +

    + {row.reference}: {row.reason} +

    + ))} +
    + ); +} diff --git a/resources/js/pages/investment/catalogue.tsx b/resources/js/pages/investment/catalogue.tsx index 5b11e346..0f658285 100644 --- a/resources/js/pages/investment/catalogue.tsx +++ b/resources/js/pages/investment/catalogue.tsx @@ -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([]); const [comparison, setComparison] = useState<{ record: Opportunity }[]>([]); const [points, setPoints] = useState(null); + const [areas, setAreas] = useState([]); 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({ />
+ + +