From fd341b627eec96a22316e645edd32d9620be433f Mon Sep 17 00:00:00 2001
From: nala
Date: Tue, 20 Oct 2026 18:53:41 +0200
Subject: [PATCH] fix: harmonizar pesquisa e navegacao dos catalogos em QA
---
.../Api/V1/OpportunityController.php | 3 +-
.../InvestmentDiscoveryController.php | 8 +-
app/Services/InvestmentCatalogue.php | 100 +-
app/Services/OpportunityClassification.php | 2 +-
app/Services/OpportunityMaturity.php | 26 +
.../opportunity-search-qa-manifest.json | 155 ++
.../unification/traceability-and-manual.md | 54 +
.../auditoria-portal/backlog-priorizado.csv | 11 +
docs/investmoz/v2/auditoria-portal/backlog.md | 5 +
.../v2/auditoria-portal/matriz-requisitos.csv | 11 +
.../components/catalogue-filter-context.tsx | 140 ++
.../js/components/catalogue-navigation.tsx | 56 +-
resources/js/components/country-select.tsx | 9 +-
.../js/components/investment-map-panel.tsx | 16 +-
.../js/components/investment-territory.tsx | 26 +-
.../components/opportunity-classification.tsx | 11 +-
.../components/opportunity-search-filters.tsx | 480 ++++++
resources/js/components/search-feedback.tsx | 195 ++-
resources/js/components/search-state.tsx | 51 +-
resources/js/lib/catalogue-context.ts | 12 +
resources/js/lib/portal-navigation.ts | 37 +
.../pages/investment/business-marketplace.tsx | 177 ++-
resources/js/pages/investment/catalogue.tsx | 1284 ++++-------------
resources/js/pages/investment/detail.tsx | 9 +-
resources/js/pages/investment/shared.tsx | 1 +
resources/js/pages/investment/zone.tsx | 21 +-
resources/js/pages/investment/zones.tsx | 141 +-
resources/js/pages/portal/services.tsx | 319 ++--
resources/js/pages/portfolio/index.tsx | 33 +-
resources/js/pages/portfolio/shared.tsx | 12 +-
resources/js/pages/portfolio/show.tsx | 8 +
tests/Feature/OpportunitySearchTest.php | 92 ++
tests/Support/OpportunitySearchFixture.php | 49 +
tests/frontend/portal-navigation.test.mjs | 67 +-
34 files changed, 2311 insertions(+), 1310 deletions(-)
create mode 100644 docs/investmoz/unification/opportunity-search-qa-manifest.json
create mode 100644 resources/js/components/catalogue-filter-context.tsx
create mode 100644 resources/js/components/opportunity-search-filters.tsx
create mode 100644 resources/js/lib/catalogue-context.ts
create mode 100644 tests/Feature/OpportunitySearchTest.php
create mode 100644 tests/Support/OpportunitySearchFixture.php
diff --git a/app/Http/Controllers/Api/V1/OpportunityController.php b/app/Http/Controllers/Api/V1/OpportunityController.php
index 1308218f..bce21579 100644
--- a/app/Http/Controllers/Api/V1/OpportunityController.php
+++ b/app/Http/Controllers/Api/V1/OpportunityController.php
@@ -36,6 +36,7 @@ class OpportunityController extends Controller
$term = $validated['q'];
$query->where(fn ($q) => $q
->whereLike('title', "%{$term}%")
+ ->orWhereLike('description', "%{$term}%")
->orWhereLike('summary', "%{$term}%")
->orWhereLike('reference', "%{$term}%")
->orWhereLike('province', "%{$term}%")
@@ -54,7 +55,7 @@ class OpportunityController extends Controller
}
if (! empty($validated['maturity_level'])) {
- $query->where('maturity_level', $validated['maturity_level']);
+ \App\Services\OpportunityMaturity::constrainSearch($query, 'historical:'.$validated['maturity_level']);
}
$results = $query->orderByDesc('is_featured')
diff --git a/app/Http/Controllers/InvestmentDiscoveryController.php b/app/Http/Controllers/InvestmentDiscoveryController.php
index 25300c54..db9cd8ad 100644
--- a/app/Http/Controllers/InvestmentDiscoveryController.php
+++ b/app/Http/Controllers/InvestmentDiscoveryController.php
@@ -53,7 +53,7 @@ class InvestmentDiscoveryController extends Controller
if (! empty($filters['geo_location_id'])) {
app(InvestmentGeography::class)->constrainDossiersToTerritories($query, app(InvestmentGeography::class)->descendants($filters['geo_location_id']));
}
- foreach (['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'zee_id', 'zfi_id', 'park_id', 'education', 'facets', 'sector', 'province', 'district', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level'] as $field) {
+ foreach (['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'zee_id', 'zfi_id', 'park_id', 'education', 'facets', 'sector', 'province', 'district', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation'] as $field) {
if (isset($filters[$field]) && $filters[$field] !== '' && $filters[$field] !== false && $filters[$field] !== []) {
return $query->whereRaw('false')->get();
}
@@ -75,9 +75,9 @@ class InvestmentDiscoveryController extends Controller
return response()->json($catalogue->query($catalogue->filters($request))->paginate($request->integer('per_page', 12))->withQueryString());
}
- public function references(InvestmentCatalogue $catalogue): JsonResponse
+ public function references(Request $request, InvestmentCatalogue $catalogue): JsonResponse
{
- return response()->json($catalogue->references());
+ return response()->json($catalogue->references(false, $catalogue->filters($request)));
}
public function detail(Request $request, string $slug, InvestmentCatalogue $catalogue): Response
@@ -131,7 +131,7 @@ class InvestmentDiscoveryController extends Controller
{
$filters = $catalogue->filters($request);
$context = $this->territorial($filters)->map(fn ($d) => ['id' => 'dossier:'.$d->id, 'title' => $d->title, 'kind' => $d->cluster_id ? 'Cluster' : ($d->corridor_id ? 'Corredor' : 'Projeto âncora'), 'url' => '/carteira/'.$d->id]);
- $opportunityOnlyFilters = array_filter(Arr::only($filters, ['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
+ $opportunityOnlyFilters = array_filter(Arr::only($filters, ['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
if ($opportunityOnlyFilters === [] && ! in_array($filters['dimension'] ?? null, ['cluster', 'corridor'], true)) {
$zoneFilters = $filters;
if (in_array($filters['dimension'] ?? null, ['zee', 'zfi'], true)) {
diff --git a/app/Services/InvestmentCatalogue.php b/app/Services/InvestmentCatalogue.php
index b8efa15c..0238cc1e 100644
--- a/app/Services/InvestmentCatalogue.php
+++ b/app/Services/InvestmentCatalogue.php
@@ -18,6 +18,7 @@ class InvestmentCatalogue
public function filters(Request $request): array
{
$filters = $request->validate(['q' => ['nullable', 'string', 'max:120'], 'sector' => ['nullable', 'string', 'max:80'],
+ 'preparation' => ['nullable', \Illuminate\Validation\Rule::in(array_column(OpportunityMaturity::searchOptions(), 'value'))],
'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'], 'territory_basis' => ['nullable', 'in:all,direct,context'],
@@ -40,6 +41,17 @@ class InvestmentCatalogue
'publication_state' => ['nullable', 'in:published'],
'per_page' => ['nullable', 'integer', 'between:1,50'], 'page' => ['nullable', 'integer', 'min:1']]);
app(InvestmentGeography::class)->selection($filters);
+ $aliases = ['park' => 'park_id', 'cluster' => 'cluster_id', 'corridor' => 'corridor_id', 'project' => 'project_id'];
+ $alias = $aliases[$filters['association_type'] ?? ''] ?? null;
+ if ($alias && ! empty($filters['association_target']) && empty($filters['association_purpose']) && empty($filters['association_state'])) {
+ if (! empty($filters[$alias]) && $filters[$alias] !== $filters['association_target']) {
+ throw \Illuminate\Validation\ValidationException::withMessages([$alias => 'Foram indicadas duas selecções diferentes para o mesmo filtro. Escolha apenas uma.']);
+ }
+ if (\Illuminate\Support\Str::isUuid($filters['association_target'])) {
+ $filters[$alias] = $filters['association_target'];
+ unset($filters['association_type'], $filters['association_target']);
+ }
+ }
return $filters;
}
@@ -51,6 +63,7 @@ class InvestmentCatalogue
public function query(array $filters): Builder
{
$query = Opportunity::published()->with('sector:id,name,slug');
+ $associated = $this->publicAssociatedIds($filters);
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') {
@@ -92,15 +105,17 @@ class InvestmentCatalogue
}
foreach (['zee_id' => 'zee', 'zfi_id' => 'zfi', 'park_id' => 'industrial_park'] as $field => $category) {
if (! empty($filters[$field])) {
- $query->whereIn('opportunities.id', app(InvestmentZones::class)->links()
+ $legacy = app(InvestmentZones::class)->links()->where('zl.association_type', '<>', 'proximity')
->join('investment_item_opportunities as mapping', 'mapping.item_id', '=', 'zl.item_id')
->join('investment_zones as z', 'z.id', '=', 'zl.zone_id')->where('z.category', $category)
- ->where('zl.zone_id', $filters[$field])->select('mapping.opportunity_id'));
+ ->where('zl.zone_id', $filters[$field])->select('mapping.opportunity_id');
+ $query->where(fn ($matches) => $matches->whereIn('opportunities.id', $legacy)
+ ->orWhereIn('opportunities.id', $associated[$field] ?? []));
}
}
if (! empty($filters['q'])) {
$term = '%'.$filters['q'].'%';
- $query->where(fn ($q) => $q->where('title', 'ilike', $term)->orWhere('summary', 'ilike', $term)->orWhere('reference', 'ilike', $term)
+ $query->where(fn ($q) => $q->where('title', 'ilike', $term)->orWhere('description', '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'))
@@ -116,11 +131,21 @@ class InvestmentCatalogue
if (! empty($filters['sector'])) {
SectorOrganization::constrain($query, $filters['sector']);
}
- foreach (['province', 'district', 'maturity_level'] as $field) {
+ foreach (['province', 'district'] as $field) {
if (! empty($filters[$field])) {
$query->where($field, $filters[$field]);
}
}
+ if (! empty($filters['preparation'])) {
+ OpportunityMaturity::constrainSearch($query, $filters['preparation']);
+ } elseif (! empty($filters['maturity_level'])) {
+ // Legacy links predate the eight-stage catalogue: retain their original meaning.
+ OpportunityMaturity::constrainSearch($query, match ((int) $filters['maturity_level']) {
+ 7 => 'stage:implementation',
+ 8 => 'stage:operational',
+ default => 'historical:'.$filters['maturity_level'],
+ });
+ }
foreach (self::RELATIONS as $relation => [$foreign, $table]) {
if (! empty($filters[$foreign])) {
$locationIds = $relation === 'locations'
@@ -141,13 +166,15 @@ class InvestmentCatalogue
continue;
}
if (in_array($relation, ['clusters', 'corridors'], true)) {
- $query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')
+ $query->where(fn ($matches) => $matches->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')
->whereColumn('link.opportunity_id', 'opportunities.id')->where('link.'.$foreign, $filters[$foreign])
- ->whereIn('link.'.$foreign, $this->publicStructureIds($relation, $foreign)));
+ ->whereIn('link.'.$foreign, $this->publicStructureIds($relation, $foreign)))
+ ->orWhereIn('opportunities.id', $associated[$foreign] ?? []));
} else {
- $query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')->join($table.' as target', 'target.id', '=', 'link.'.$foreign)
+ $query->where(fn ($matches) => $matches->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')->join($table.' as target', 'target.id', '=', 'link.'.$foreign)
->whereColumn('link.opportunity_id', 'opportunities.id')->where('link.'.$foreign, $filters[$foreign])->whereNull('target.deleted_at')
- ->whereIn('target.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign)));
+ ->whereIn('target.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign)))
+ ->orWhereIn('opportunities.id', $associated[$foreign] ?? []));
}
}
}
@@ -187,7 +214,9 @@ class InvestmentCatalogue
* @return array */
public function references(bool $management = false, array $territoryFilters = []): array
{
+ $path = app(InvestmentGeography::class)->selection($territoryFilters);
$result = ['classification' => app(OpportunityClassification::class)->options(),
+ 'preparationOptions' => OpportunityMaturity::searchOptions(),
'authorities' => DB::table('authorities')->where('status', 'published')->get(['id', 'label_pt_mz']),
'categories' => OpportunityManagement::CATEGORIES,
'options' => DB::table('investment_reference_options')->when(! $management, fn ($q) => $q->where('active', true))->orderBy('sort_order')->orderBy('label')->get(),
@@ -205,18 +234,67 @@ class InvestmentCatalogue
$query->whereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign));
}
}
+ if (! $management && $path !== [] && $relation !== 'locations') {
+ $dossiers = InvestmentPortfolioController::visible();
+ app(InvestmentGeography::class)->constrainDossiersToTerritories($dossiers,
+ app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id));
+ $query->whereIn('id', $dossiers->whereNotNull($foreign)->select($foreign));
+ }
$result[$relation] = $query->orderBy('name')->get(['id', 'name']);
}
$zones = app(InvestmentZones::class)->visible();
if (! $management && $territoryFilters !== []) {
- $zones = app(InvestmentZones::class)->search($territoryFilters);
+ // Opportunity words/modalities are not names/categories of zones.
+ $zones = app(InvestmentZones::class)->search(\Illuminate\Support\Arr::only($territoryFilters,
+ ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'geo_location_id']));
}
$result['zones'] = $zones->orderBy('name')->get(['id', 'name', 'category']);
+ if (! $management && ($path !== [] || ! empty($territoryFilters['sector']))) {
+ $scope = \Illuminate\Support\Arr::only($territoryFilters, ['sector','province_id','district_id','admin_post_id','locality_id','include_national']);
+ $available = ['chain' => [], 'activity' => []];
+ $this->query($scope)->reorder()->chunkById(200, function ($records) use (&$available) {
+ foreach ($records as $record) {
+ foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
+ if (isset($available[$row['target_type']])) $available[$row['target_type']][] = $row['target_id'];
+ }
+ foreach (['subsector_id','economic_activity_id'] as $key) {
+ $id = $record->workflow_context['publication']['taxonomy'][$key] ?? null;
+ if ($id) $available['activity'][] = $id;
+ }
+ }
+ }, 'opportunities.id', 'id');
+ foreach ($available as $kind => $ids) {
+ $result['classification']['targets'][$kind] = collect($result['classification']['targets'][$kind])->whereIn('id', $ids)->values();
+ }
+ }
return $result;
}
+ private function publicAssociatedIds(array $filters): array
+ {
+ $kinds = ['park_id' => ['park'], 'zee_id' => ['zone'], 'zfi_id' => ['zone'],
+ 'cluster_id' => ['cluster'], 'corridor_id' => ['corridor'], 'project_id' => ['project']];
+ $selected = array_filter(array_intersect_key($filters, $kinds));
+ foreach (['zee_id' => 'zee', 'zfi_id' => 'zfi', 'park_id' => 'industrial_park'] as $key => $category) {
+ if (isset($selected[$key]) && ! app(InvestmentZones::class)->visible()->whereKey($selected[$key])->where('category', $category)->exists()) unset($selected[$key]);
+ }
+ if ($selected === []) return [];
+ $ids = [];
+ Opportunity::published()->select('opportunities.*')->chunkById(200, function ($records) use ($selected, $kinds, &$ids) {
+ foreach ($records as $record) {
+ foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
+ if (in_array($row['purpose'], ['proximity', 'administrative_boundary'], true) || $row['state'] === 'rejected') continue;
+ foreach ($selected as $field => $target) {
+ if ($row['target_id'] === $target && in_array($row['target_type'], $kinds[$field], true)) $ids[$field][] = $record->id;
+ }
+ }
+ }
+ });
+ return $ids;
+ }
+
private function publicStructureIds(string $relation, string $foreign): \Illuminate\Database\Query\Builder
{
$table = self::RELATIONS[$relation][1];
@@ -329,11 +407,11 @@ class InvestmentCatalogue
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();
+ 'url' => '/oportunidades/'.$area->slug.($filters === [] ? '' : '?'.http_build_query($filters))])->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];
+ 'location' => trim($record->province.' '.$record->district), 'url' => '/oportunidades/'.$record->slug.($filters === [] ? '' : '?'.http_build_query($filters))];
foreach ($relations as $relation => $groups) {
$properties[$relation] = ($groups->get($record->id) ?? collect())->pluck('name')->unique()->values()->all();
}
diff --git a/app/Services/OpportunityClassification.php b/app/Services/OpportunityClassification.php
index d55f78d4..810aa8da 100644
--- a/app/Services/OpportunityClassification.php
+++ b/app/Services/OpportunityClassification.php
@@ -383,7 +383,7 @@ class OpportunityClassification
$matches = $matches && collect($rows)->contains(fn ($r) => $r['authority_id'] === $filters['authority_id']);
}
if (! empty($filters['subsector_id'])) {
- $matches = $matches && (($record->workflow_context['publication']['taxonomy']['subsector_id'] ?? '') === $filters['subsector_id'] || collect($rows)->contains(fn ($r) => $r['target_type'] === 'activity' && $r['target_id'] === $filters['subsector_id']));
+ $matches = $matches && (in_array($filters['subsector_id'], array_intersect_key($record->workflow_context['publication']['taxonomy'] ?? [], array_flip(['subsector_id','economic_activity_id'])), true) || collect($rows)->contains(fn ($r) => $r['target_type'] === 'activity' && $r['target_id'] === $filters['subsector_id']));
}
if ($matches) {
$ids[] = $record->id;
diff --git a/app/Services/OpportunityMaturity.php b/app/Services/OpportunityMaturity.php
index 53a5ae73..e34975c5 100644
--- a/app/Services/OpportunityMaturity.php
+++ b/app/Services/OpportunityMaturity.php
@@ -24,6 +24,32 @@ final class OpportunityMaturity
public const LEGACY_LABELS = [1 => 'Ideia identificada', 'Pré-estudo disponível', 'Estudo de viabilidade em curso', 'Projecto estruturado', 'Pronto para investimento', 'Em negociação ou implementação'];
+ /** Public selection is explicit; historical numbers never stand for current stages. */
+ public static function searchOptions(): array
+ {
+ $options = [];
+ foreach (self::STAGES as $code => $label) {
+ $options[] = ['value' => 'stage:'.$code, 'label' => $label, 'group' => 'Etapas de preparação'];
+ }
+ foreach (self::LEGACY_LABELS as $level => $label) {
+ $options[] = ['value' => 'historical:'.$level, 'label' => $label.' — registo histórico', 'group' => 'Classificações históricas'];
+ }
+ return $options;
+ }
+
+ public static function constrainSearch(\Illuminate\Database\Eloquent\Builder $query, string $selection): void
+ {
+ [$kind, $value] = explode(':', $selection, 2);
+ if ($kind === 'stage') {
+ $query->where('workflow_context->publication->maturity->catalogue_version', self::VERSION)
+ ->where('workflow_context->publication->maturity->stage', $value);
+ } else {
+ $query->where(fn ($q) => $q->where('workflow_context->publication->maturity->catalogue_version', self::LEGACY)
+ ->orWhere(fn ($old) => $old->whereNull('workflow_context->publication->maturity->catalogue_version')->whereNull('workflow_context->publication->maturity->stage')))
+ ->where('maturity_level', (int) $value);
+ }
+ }
+
/** @param array $data
* @return array */
public function normalize(array $data): array
diff --git a/docs/investmoz/unification/opportunity-search-qa-manifest.json b/docs/investmoz/unification/opportunity-search-qa-manifest.json
new file mode 100644
index 00000000..941280f6
--- /dev/null
+++ b/docs/investmoz/unification/opportunity-search-qa-manifest.json
@@ -0,0 +1,155 @@
+{
+ "branch": "codex/opportunity-search",
+ "base_commit": "1350b4df73e70ab35a28d923402ded7d836ade38",
+ "date": "2026-10-20",
+ "scope": "QA only; review before main integration",
+ "qa_url": "http://127.0.0.1:18570/oportunidades",
+ "files": [
+ {
+ "path": "app/Http/Controllers/Api/V1/OpportunityController.php",
+ "sha256": "04fd696ce73fae8660491976781ef1bd96898a30af96363460427e7a9d139fe1"
+ },
+ {
+ "path": "app/Http/Controllers/InvestmentDiscoveryController.php",
+ "sha256": "a7301ce6c6285d8ec374b7013b8f0a0bb9951da83625acc47da223d2c4275bbf"
+ },
+ {
+ "path": "app/Services/InvestmentCatalogue.php",
+ "sha256": "4b44e18ca12c890e0220913c972ca6092e29f8975dfbc6d292b579b95f1c5e21"
+ },
+ {
+ "path": "app/Services/OpportunityClassification.php",
+ "sha256": "f9a24067807fa620142839ab9873dde465457fcfc61f501d1c8309534a6da175"
+ },
+ {
+ "path": "app/Services/OpportunityMaturity.php",
+ "sha256": "1cd73acb486d8e3ffa52c7419e48b9d69b450bb343f04664cd1a2b51e3317246"
+ },
+ {
+ "path": "docs/investmoz/unification/traceability-and-manual.md",
+ "sha256": "e5400717643bde98a3b1e1163c4a4ebd4c6a774a168a3f5430c46cc3f850f5d8"
+ },
+ {
+ "path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
+ "sha256": "5d1de3418c5b021a0520bc6494a6a5efcef665ad1d20a45b11ae6aa702bedb01"
+ },
+ {
+ "path": "docs/investmoz/v2/auditoria-portal/backlog.md",
+ "sha256": "303b53346af2dad908f9d40d02c14a8f245155897dacf8b2538cdaae5a92bea7"
+ },
+ {
+ "path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
+ "sha256": "4bbf47f7b5bfe54d2dd3b891eca3c04555329e92064c3bc6722a60c2f8eab4ce"
+ },
+ {
+ "path": "resources/js/components/catalogue-filter-context.tsx",
+ "sha256": "b155d5e812b52fc52677cb9a8d44068f64821732c90a82dabb521eed44af047e"
+ },
+ {
+ "path": "resources/js/components/catalogue-navigation.tsx",
+ "sha256": "88eccc6a9c2803fd26b678081f5da244a9c4ac9227664e7d0f1b57050b437239"
+ },
+ {
+ "path": "resources/js/components/country-select.tsx",
+ "sha256": "53ef3742e4c55668d0ad85139be742405f5d1524196b3b1b3f3700c7bc311742"
+ },
+ {
+ "path": "resources/js/components/investment-map-panel.tsx",
+ "sha256": "cfdb99b7dfa405d740f68cecef2b0df38488d2f63dc623da47a000037f0ec01e"
+ },
+ {
+ "path": "resources/js/components/investment-territory.tsx",
+ "sha256": "35cef961efb51709cba271556f5e7c717b673ebbca480b680e37654c38cde324"
+ },
+ {
+ "path": "resources/js/components/opportunity-classification.tsx",
+ "sha256": "fb4d843c8c056f10c34baa5ab9f2ba324e9796456a20455303b7a3b0747ee6cd"
+ },
+ {
+ "path": "resources/js/components/opportunity-search-filters.tsx",
+ "sha256": "85e48e41537a5932e0398b7bc4bddd5322431ae517a91de2d53e83ef7927e54d"
+ },
+ {
+ "path": "resources/js/components/search-feedback.tsx",
+ "sha256": "23ed85878e5b55a40e01928236f870f6dff4c2a7e19547beaa81a0a7d59c9c06"
+ },
+ {
+ "path": "resources/js/components/search-state.tsx",
+ "sha256": "45cc210783625bcf96712064a0e7dd97f5681c499cdf60cedbe0e4127cba7aab"
+ },
+ {
+ "path": "resources/js/lib/catalogue-context.ts",
+ "sha256": "c499ebda27b7672b55ea3e3da98c63c7d2d2cd490dcde77df3cc38e09792af73"
+ },
+ {
+ "path": "resources/js/lib/portal-navigation.ts",
+ "sha256": "0d3bae6d726538b064cd992e6d433245321951c18198de5db540993232e6daaf"
+ },
+ {
+ "path": "resources/js/pages/investment/business-marketplace.tsx",
+ "sha256": "fb6917c095f5ade85f52adeaf1292486627097e4f0daf0211f2b3627620ccd85"
+ },
+ {
+ "path": "resources/js/pages/investment/catalogue.tsx",
+ "sha256": "9ea5742950e2737bebb7bd7f04ee24129c7f9917c1986cf070a3e282e7a22981"
+ },
+ {
+ "path": "resources/js/pages/investment/detail.tsx",
+ "sha256": "c40926782a183fcde27415e0eda276f73189f071ba6ce715e79d007eb6388634"
+ },
+ {
+ "path": "resources/js/pages/investment/shared.tsx",
+ "sha256": "f71d13f0e0b5b0e152cf262f02b9e0770a58d32472ac878aa0adbca53d2edf3f"
+ },
+ {
+ "path": "resources/js/pages/investment/zone.tsx",
+ "sha256": "8cd64fb62af5e2d4582c46298568429e61e45247fca1b072287ad35ae07d76db"
+ },
+ {
+ "path": "resources/js/pages/investment/zones.tsx",
+ "sha256": "45244ce4be7e75b4d52c453f90d1764cd4d7a5ab4299d6f0af91c68fbf103ebd"
+ },
+ {
+ "path": "resources/js/pages/portal/services.tsx",
+ "sha256": "6f26cace5d1808f6d563d0d8b0ebed6b44748abed7205e465d9658f8b9cb2d17"
+ },
+ {
+ "path": "resources/js/pages/portfolio/index.tsx",
+ "sha256": "2662cfe05c387cb876f8c352bd83a5a1d68056c615333e238ecb4e7b5489eb00"
+ },
+ {
+ "path": "resources/js/pages/portfolio/shared.tsx",
+ "sha256": "27b9d1c8dbced48532e7a57ce184810a32e3dea7cdc3a37a549dc7fbe8c04426"
+ },
+ {
+ "path": "resources/js/pages/portfolio/show.tsx",
+ "sha256": "51883858d09c6b07f281426fa9ec149c4de9f12286cc88b285caee01e892c892"
+ },
+ {
+ "path": "tests/Feature/OpportunitySearchTest.php",
+ "sha256": "b2d57a838849a7d1c9e5b43c3f471d3e5d78c59a7cdb8e0f81f74de5d886d7d3"
+ },
+ {
+ "path": "tests/Support/OpportunitySearchFixture.php",
+ "sha256": "46804da2f403c031ad206211db07f41f3f20dbf4f789f838e278e3f241d021f1"
+ },
+ {
+ "path": "tests/frontend/portal-navigation.test.mjs",
+ "sha256": "f19ff2d17a385ab1f28c65f69233a13d88d6b8b6ec7285848fcd2a945b883ff9"
+ }
+ ],
+ "build_manifest_sha256": "2e2113d46d7f89ff94851b88a8d023ca4427d57d4f1361bf9577aef8d3aa64af",
+ "migrations": [],
+ "data_operations": [],
+ "remote_operations": [],
+ "new_dependencies": [],
+ "permissions_changes": false,
+ "proofs": "outputs/opportunity-search-20261020/receipt.json",
+ "pending": [
+ "Tempo review before main integration",
+ "Natural-language interpretation not implemented; evidence_only remains separate",
+ "Eight contractual gaps and P7 unchanged"
+ ],
+ "rollback": "Restore previous source/build using existing release chain; keep database, uploads, permissions and later data.",
+ "hash_basis": "Git index bytes; line endings normalized by repository attributes. Manifest itself excluded."
+}
diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md
index 5c79c1ce..d1ea6d9d 100644
--- a/docs/investmoz/unification/traceability-and-manual.md
+++ b/docs/investmoz/unification/traceability-and-manual.md
@@ -1465,3 +1465,57 @@ Recuperação: parar a path unit, aguardar o executor e usar `python3 /var/www/d
As correcções do ensaio limitaram-se à composição: diagnóstico mais recente dos programadores, teste de exemplo sem migração inútil, fixtures unitárias independentes das flags do destino, expectativas dos menus/relatórios já aceites e guarda do cookie fora do navegador, com prova de CSRF preservado. Nenhuma nova regra organizacional.
Provas: outputs/investmoz-git-delivery-20261020/RECIBO-DISPONIBILIZACAO-GIT-MAIN.json; PROVA-ENSAIO-COMPLETO.json; PROVA-CONTROLO-FINAL-LINUX.json; PROVA-GITEA-EVENTO-E-INSTALACAO.json; PROVA-VISUAL-GIT-MAIN.png; deploy/homologacao/README.md; /build/revision.json. Manual versionado: `deploy/homologacao/README.md`. Esta entrega encerra o fluxo automático, não os restantes componentes da aplicação. As alterações documentais anteriores no checkout local foram preservadas.
+
+
+## Pesquisa transversal QA — 20/10/2026
+
+Origem: instruções expressas da Tempo nesta conversa. A classificação multidimensional, dados, permissões, histórico e URLs mantêm-se. Os ajustes de interface/consulta são implementação do pedido, não novas regras organizacionais. Branch `codex/opportunity-search`, base `1350b4df`, QA HTTP `http://127.0.0.1:18570`. Não integrar em `main` antes da revisão: a integração desencadeia disponibilização automática.
+
+**Causa reproduzida de 11 versus 0:** em `/oportunidades?sector=turismo` no destino, o selector provincial apresentava 11 opções através de `SearchState` como “11 resultados encontrados”, enquanto o paginador devolvia zero oportunidades públicas. São universos diferentes. Agora o primeiro diz “Província: 11 opções disponíveis”; o catálogo usa apenas o total do seu paginador. Não foram alterados dados para obter concordância. Foram ainda retirados o resumo global duplicado desta página, sugestões com critérios incompletos e resultados de parcerias que tinham pesquisa própria.
+
+| Ecrã/URL | Problema | Correcção | Prova | Pendência/limite |
+|---|---|---|---|---|
+| Oportunidades — /oportunidades | Filtros duplicados, códigos internos, maturidades cruzadas, contagens concorrentes e mapa em grelha. | Filtro único, Mais filtros, preparação explícita, moeda obrigatória para montantes, pedidos canceláveis, estados e contexto únicos. | 6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado. | Revisão visual da Tempo; NLP separado. |
+| Negócios — /negocios | Classificação técnica visível, estados de auditoria e contagem territorial sem âmbito. | Sector básico, critérios especializados recolhidos em Mais filtros, estado de auditoria retirado do controlo público, contagem identificada e filtros removíveis. | HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados. | Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote. |
+| Sectores — /sectores; /sectores/{slug} | Navegação relacionada dispersa e contagens genéricas sem nome do universo. | Contagem identificada como Sectores; navegação comum preserva a entrada por sector e Fintech como especialização. | Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação. | Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro. |
+| Localizações — /oportunidades?view=map | Mapa e contagem de oportunidades podiam usar universos diferentes sem explicação. | Mesmos filtros e permissões; locais com coordenadas e oportunidades sem coordenadas apresentados separadamente; fichas de contexto identificadas. | Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas. | Nenhuma coordenada ou geometria criada. |
+| Carteira — /carteira | Menu local repetido, ausência de retorno explícito e de distinção entre ficha e pesquisa associada. | Contagem Fichas da carteira, chips removíveis, menu local redundante retirado, Consultar ficha e Ver oportunidades associadas; retorno com query local. | HTTP/Inertia de vazio; navegador de vazio; teste de contexto e navegação. | Provas anteriores de edição/publicação conservadas; não repetidas. |
+| Zonas e parques — /zonas; /zonas/{id} | Destinos repetidos, vazio atribuído indevidamente a falta de cadastro, retorno sem filtros. | Destinos relacionados recolhidos; vazio neutro; consultar ficha separado de oportunidades associadas; retorno e exportação preservam filtros. | HTTP/Inertia de vazio; inspecção da query/exportação e helpers de retorno. | Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha. |
+| Clusters — /clusters → /carteira?kind=cluster | Mesmo padrão da carteira; não é um catálogo independente de oportunidades. | Reutiliza carteira, contagem nomeada e tipo de ficha removível; URL anterior preservada. | Redireccionamento verificado no teste PHP; consumidor comum inspeccionado. | Nenhuma equivalência entre implantação e associação criada. |
+| Corredores — /corredores → /carteira?kind=corridor | Mesmo padrão da carteira e risco de confundir ficha territorial com oportunidade. | Reutiliza carteira e ligações com finalidade explícita; URL anterior preservada. | Redireccionamento verificado no teste PHP; consumidor comum inspeccionado. | Nenhuma alteração de relações ou dados. |
+| Educação — /educacao | Herda formulário e contagens concorrentes das oportunidades. | Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido. | HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns. | Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões. |
+| Biblioteca — /documentos | Dois resumos de contagem, filtros activos sem remoção individual, controlos sem opções e rodapé local repetido. | Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum. | HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado. | Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva. |
+| Legislação — /legislacao | Mesmo padrão da biblioteca; não deve expor um universo concorrente. | Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes. | HTTP/Inertia: resources.total=0; consumidor e query pública partilhados. | Situação jurídica, consulta e cálculo continuam independentes. |
+
+
+### Utilização
+
+1. Em oportunidades, preencher palavras, sector, modalidade e localização até ao nível conhecido. Usar “Mais filtros” para estruturas, actividades e cadeias. As opções territoriais não são a contagem das oportunidades.
+2. Para limites financeiros, escolher a moeda. Sem moeda, a pesquisa pede esse campo; não assume MZN nem converte montantes. Preparação tem etapas actuais e classificações históricas separadas.
+3. Retirar um critério pelo seu botão/ligação ou usar “Limpar filtros”. Alterar o pai territorial limpa os descendentes incompatíveis. Abrir a ficha e usar “Voltar à pesquisa” recupera os critérios.
+4. “Consultar ficha” consulta a estrutura territorial/documental; “Ver oportunidades associadas” pesquisa vínculos publicados. Proximidade ou o mesmo distrito não demonstram pertença. No mapa, registos sem coordenadas continuam identificados na lista, sem posições inventadas.
+5. Durante o pedido aparece “A pesquisar…”. Um erro apresenta “Tentar novamente” e preserva critérios; não aparece como resultado vazio. Pedidos anteriores são cancelados/ignorados ao mudar os filtros. Os estados têm `status`/`alert` e `aria-live`.
+
+### Compatibilidade e provas
+
+- A pesquisa antiga `maturity_level=1..6` conserva a classificação histórica, sem misturar a posição com a etapa actual. As posições 7/8, exclusivas das oito etapas, correspondem explicitamente a implementação/operacional. Novas selecções usam código de etapa ou classificação histórica explícita, sem reescrever os registos.
+- Balama real, ID `ed4d2fba-95d5-42e3-8ca7-1f520de1036a`: leitura pública do destino devolveu zero oportunidades associadas. Na cópia QA, essa ficha real está em rascunho; não foi publicada. A prova positiva usa um parque e oportunidades **SINTÉTICO QA**, incluindo associação preliminar, proximidade, território comum, outro parque e rascunho privado. Só a associação pública correspondente entra no filtro.
+- QA isolado `opportunity_search_20261020_test`, PostgreSQL local porta 55441; guarda impede testes na base activa. Testes transaccionais revertem apenas fixtures próprias; não foi executado `migrate:fresh`.
+- Seis testes PHP, 112 asserções: associação, confidencialidade, modalidades, maturidades distintas, moeda/combinações/vazio, referências, lista/exportação e HTTP dos consumidores/aliases. Seis testes frontend: navegação, flags, destinos únicos, quatro entradas e retorno local com query. Build e TypeScript conferidos no recibo final.
+- Navegador: consulta → ficha → retorno; parque isolado; validação de moeda → vazio; falha HTTP sintética de um único pedido → nova tentativa bem-sucedida; Inhambane/Vilankulo → Gaza limpa distrito e postos. A falha existe apenas no router privado do servidor QA, fora do pacote.
+- Móvel: dimensão real 390 px, largura de conteúdo 375 px, sem transbordo horizontal; teclado abre/fecha Mais filtros e Descoberta. A capacidade automática de viewport não aplicou a dimensão; a prova utilizou o modo responsivo do navegador. Não se atribui essa limitação à aplicação.
+- Antes/depois e estados: artefactos locais `opportunity-search-artifacts/{before-search.png,before-search-ax.txt,qa-after.png,qa-mobile.png,qa-error.png,qa-zero.png,backend-tests.log}`. As capturas do destino e QA usam acervos diferentes; os totais entre ambientes não são uma comparação de dados.
+- Inventário de reutilização de `investment-territory.tsx`: oportunidades, negócios, carteira, zonas, educação, análise educativa, formulário/recursos/editor de oportunidades, estruturas, associações territoriais, contexto do investimento, pedido do Balcão, terra/DUAT, formulários públicos e assistência existente. A alteração comum identifica o âmbito da contagem; não demonstra de novo edição/autorização desses consumidores privados. `CountrySelect` também identifica Países; `InvestmentMapPanel` identifica o universo cartográfico.
+- “Catatálogo” não foi encontrado nas fontes actuais inspeccionadas. O título servido usa “Catálogo”. O menu local redundante da carteira que exibia “InvestMoz” foi retirado; a identidade comum continua InvesteMoz, propriedade da Tempo.
+
+### Assistência à pesquisa — saldo separado
+
+`config/investmoz_ai.php`, `AiGateway` e `AssistiveRules` mostram pesquisa lexical/evidências e regras sobre campos estruturados, com `model=null`, sem pesquisa semântica. Não interpretam a frase “turismo em Inhambane até 500 mil USD” como filtros. `evidence_only` não comprova NLP. Nenhum serviço externo, custo ou envio de dados foi activado.
+
+Incremento ainda necessário: interpretação delimitada de palavras/sector/geografia/montante/moeda; prévia editável dos filtros; pergunta apenas quando ambíguo; confirmação antes de executar exactamente a query manual; ausência de resultados sem alargamento silencioso. Estimativa de engenharia: 6–10 h para um interpretador local delimitado e casos de ambiguidade/testes; compreensão aberta com modelo externo exige proposta e decisão próprias. Esta estimativa não inclui as oito lacunas contratuais nem P7 e não bloqueia as correcções manuais.
+
+### Integração e reversão
+
+Rever a branch e o manifesto nominal; reconciliar novos commits dos programadores antes da integração. Após decisão da Tempo, a cadeia Git existente compila a revisão aprovada de fontes e build. Esta execução não envia para `main`, não escreve no servidor e não altera conteúdos, schema, contas, papéis, MFA ou configuração.
+
+Não há migrations, importação ou publicação neste pacote. A reversão repõe as fontes/build da revisão anterior através da cadeia existente, preservando a base, uploads, referências, permissões e dados posteriores. Os helpers de contexto só transportam query local; não aceitam URL externa como destino de retorno. O manifesto lista fontes e hash do build QA; não inclui `.env`, credenciais, dumps, vendor/node_modules ou fixtures persistentes.
diff --git a/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv b/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv
index effb733b..ba423088 100644
--- a/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv
+++ b/docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv
@@ -7370,3 +7370,14 @@ P2,AUD-COMPANY-02,Sem evidência suficiente,Modelos societários específicos ai
P2,AUD-CAPITAL-01,Não implementado,Guião estruturado de capitalização não implementado.,Biblioteca e passos existentes do percurso,"Documentos/entidades/fontes/parâmetros próprios antes de implementação; sem novos módulos nesta auditoria. Guião com opções, riscos, preparação/documentos e fonte aplicável; sem promessa de retorno. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","Consulta pública com fontes/versões, instrumentos distinguidos, riscos/documentos e próxima ação; guardar referência no dossier correto; sem recomendar sucesso nem assumir compromisso.",Preservar fechos; correções concretas→provas de consumidor→decisões isoladas→incrementos futuros,não implementado,"Não foi demonstrado guia estruturado de capitalização. O pedido não autoriza custódia, intermediação, promessa de retorno ou aprovação dos módulos de oferta.","Reutilizar biblioteca/guia público, dossier e parceiros autorizados; manter separado de ofertas experimentais. RF-CRM-009 e regras de facilitação são relações existentes, sem equivalência automática com este guia.",Exemplar v1(3) ainda não confirmado; fontes/dados/atos concretos próprios. Nenhuma nova regra ou divulgação inferida.,CAP-001 / incremento posterior ao workflow das seis categorias,[Segmentação bf26caeee043 — identidade confirmada; nome local v1.docx] Alias documental CAP-001; SEG-bf26caee-p153; SEG-bf26caee-p154; SEG-bf26caee-p155; SEG-bf26caee-p702,"Orientação sobre private equity e capitalização, preparação, riscos, documentos e percurso",Biblioteca e passos existentes do percurso,Guião estruturado de capitalização não implementado.,"Documentos/entidades/fontes/parâmetros próprios antes de implementação; sem novos módulos nesta auditoria. Guião com opções, riscos, preparação/documentos e fonte aplicável; sem promessa de retorno.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"Nova funcionalidade/expansão; preparar execução, não implementar nesta auditoria",Estimativa partilhada por lote em visao-consolidada-modulos.md; não somar por frase. Grupo: incrementos futuros.,incrementos futuros,segmentacao-processos-reconciliacao-20261011.md; contratos/rotas inventariados atuais; não há prova de percurso completo deste incremento.,,,,,16–24 h após conteúdo/âmbito definidos; não inclui intermediação.
P2,AUD-CAPITAL-02,Não implementado,"Guião da Bolsa por segmento, intervenientes e requisitos não implementado.","Biblioteca, checklist e percurso existentes","Documentos/entidades/fontes/parâmetros próprios antes de implementação; sem novos módulos nesta auditoria. Fonte por requisito, condições/períodos identificados e decisões externas separadas. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","Etapas, entidade, base/artigo/redação, documentos, requisitos por segmento e lacunas explícitas; recuperação no dossier e encaminhamento permitido, sem integração externa simulada.",Preservar fechos; correções concretas→provas de consumidor→decisões isoladas→incrementos futuros,não implementado,"Guião por segmento, documentos, condições e fonte aplicável ainda não demonstrado. Não inferir valores, prazos, prospectos, taxas ou aprovação pelos artigos1–3.",Reutilizar guia/biblioteca/dossier/checklists. Relacionar pedido de admissão e patrocínio às referências específicas; separar ação interna do portal e ato de entidade competente.,Exemplar v1(3) ainda não confirmado; fontes/dados/atos concretos próprios. Nenhuma nova regra ou divulgação inferida.,CAP-002 / incremento posterior ao workflow das seis categorias,[Segmentação bf26caeee043 — identidade confirmada; nome local v1.docx] Alias documental CAP-002; SEG-bf26caee-p153; SEG-bf26caee-p154; SEG-bf26caee-p155; SEG-bf26caee-p709,"Guião de admissão à Bolsa de Valores de Moçambique, distinguindo Bolsa, regulador e operadores","Biblioteca, checklist e percurso existentes","Guião da Bolsa por segmento, intervenientes e requisitos não implementado.","Documentos/entidades/fontes/parâmetros próprios antes de implementação; sem novos módulos nesta auditoria. Fonte por requisito, condições/períodos identificados e decisões externas separadas.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"Nova funcionalidade/expansão; preparar execução, não implementar nesta auditoria",Estimativa partilhada por lote em visao-consolidada-modulos.md; não somar por frase. Grupo: incrementos futuros.,incrementos futuros,segmentacao-processos-reconciliacao-20261011.md; contratos/rotas inventariados atuais; não há prova de percurso completo deste incremento.,,,,,16–32 h após correspondências normativas; não preencher taxas/prazos ausentes.
P2,AUD-CHECKLIST-01,Implementado parcialmente,Checklists existem em configuração; editor/versionamento administrativo sem código e adaptação de submissões antigas não demonstrados.,"Configurações existentes, schema, biblioteca e auditoria","Documentos/entidades/fontes/parâmetros próprios antes de implementação; sem novos módulos nesta auditoria. Publicar versão autorizada, novas submissões usam-na e anteriores preservam a versão aplicada. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","Gestor autorizado configura/previsualiza/publica versão, investidor recebe versão aplicável ao contexto/data, guarda incompleto e recupera; nova versão não modifica submissão anterior nem trata decisão interna como licença.",Preservar fechos; correções concretas→provas de consumidor→decisões isoladas→incrementos futuros,parcial,"Checklists e catálogos configurados existentes; editor sem alterar código/publicação versionada por entidade não demonstrados. Separar obrigações comerciais, registo/publicidade e licença setorial.","Reutilizar config/document-requirements.php, requisitos/formulários/documentos/tarefas e serviços do Balcão existentes. Não duplicar motor, tabelas ou as cinco fases/19 etapas.",Exemplar v1(3) ainda não confirmado; fontes/dados/atos concretos próprios. Nenhuma nova regra ou divulgação inferida.,INV-007 / incremento posterior ao workflow das seis categorias,[Segmentação bf26caeee043 — identidade confirmada; nome local v1.docx] Alias documental INV-007; SEG-bf26caee-p14; SEG-bf26caee-p15; SEG-bf26caee-p20; SEG-bf26caee-p23; SEG-bf26caee-p26; SEG-bf26caee-p29; SEG-bf26caee-p32; SEG-bf26caee-p35; SEG-bf26caee-p38; SEG-bf26caee-p41; SEG-bf26caee-p44; SEG-bf26caee-p47; SEG-bf26caee-p50; SEG-bf26caee-p53; SEG-bf26caee-p56; SEG-bf26caee-p59; SEG-bf26caee-p62; SEG-bf26caee-p65; SEG-bf26caee-p68; SEG-bf26caee-p71; SEG-bf26caee-p74; SEG-bf26caee-p77; SEG-bf26caee-p80; SEG-bf26caee-p83; SEG-bf26caee-p87; SEG-bf26caee-p104; SEG-bf26caee-p109; SEG-bf26caee-p114; SEG-bf26caee-p119; SEG-bf26caee-p124; SEG-bf26caee-p129; SEG-bf26caee-p134; SEG-bf26caee-p140; SEG-bf26caee-p144; SEG-bf26caee-p145; SEG-bf26caee-p146; SEG-bf26caee-p147; SEG-bf26caee-p160; SEG-bf26caee-p161; SEG-bf26caee-p163; SEG-bf26caee-p164; SEG-bf26caee-p165; SEG-bf26caee-p166; SEG-bf26caee-p167; SEG-bf26caee-p168; SEG-bf26caee-p169; SEG-bf26caee-p170; SEG-bf26caee-p176; SEG-bf26caee-p180; SEG-bf26caee-p184; SEG-bf26caee-p188; SEG-bf26caee-p192; SEG-bf26caee-p196; SEG-bf26caee-p200; SEG-bf26caee-p204; SEG-bf26caee-p208; SEG-bf26caee-p212; SEG-bf26caee-p216; SEG-bf26caee-p220; SEG-bf26caee-p226; SEG-bf26caee-p227; SEG-bf26caee-p228; SEG-bf26caee-p229; SEG-bf26caee-p230; SEG-bf26caee-p231; SEG-bf26caee-p232; SEG-bf26caee-p233; SEG-bf26caee-p234; SEG-bf26caee-p237; SEG-bf26caee-p242; SEG-bf26caee-p247; SEG-bf26caee-p252; SEG-bf26caee-p257; SEG-bf26caee-p262; SEG-bf26caee-p267; SEG-bf26caee-p272; SEG-bf26caee-p277; SEG-bf26caee-p282; SEG-bf26caee-p289; SEG-bf26caee-p293; SEG-bf26caee-p297; SEG-bf26caee-p301; SEG-bf26caee-p305; SEG-bf26caee-p309; SEG-bf26caee-p313; SEG-bf26caee-p317; SEG-bf26caee-p321; SEG-bf26caee-p325; SEG-bf26caee-p329; SEG-bf26caee-p338; SEG-bf26caee-p342; SEG-bf26caee-p346; SEG-bf26caee-p350; SEG-bf26caee-p354; SEG-bf26caee-p358; SEG-bf26caee-p362; SEG-bf26caee-p366; SEG-bf26caee-p370; SEG-bf26caee-p374; SEG-bf26caee-p378; SEG-bf26caee-p382; SEG-bf26caee-p386; SEG-bf26caee-p390; SEG-bf26caee-p394; SEG-bf26caee-p398; SEG-bf26caee-p402; SEG-bf26caee-p407; SEG-bf26caee-p410; SEG-bf26caee-p481; SEG-bf26caee-p565; SEG-bf26caee-p648; SEG-bf26caee-p681; SEG-bf26caee-p756; SEG-bf26caee-p759; SEG-bf26caee-p762; SEG-bf26caee-p765; SEG-bf26caee-p768; SEG-bf26caee-p771; SEG-bf26caee-p787; SEG-bf26caee-p788; SEG-bf26caee-p789; SEG-bf26caee-p790; SEG-bf26caee-p791; SEG-bf26caee-p792,"Editor de checklists e formulários configuráveis, publicação de versões e preservação de submissões anteriores","Configurações existentes, schema, biblioteca e auditoria",Checklists existem em configuração; editor/versionamento administrativo sem código e adaptação de submissões antigas não demonstrados.,"Documentos/entidades/fontes/parâmetros próprios antes de implementação; sem novos módulos nesta auditoria. Publicar versão autorizada, novas submissões usam-na e anteriores preservam a versão aplicada.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"Nova funcionalidade/expansão; preparar execução, não implementar nesta auditoria",Estimativa partilhada por lote em visao-consolidada-modulos.md; não somar por frase. Grupo: incrementos futuros.,incrementos futuros,segmentacao-processos-reconciliacao-20261011.md; contratos/rotas inventariados atuais; não há prova de percurso completo deste incremento.,,,,,"24–40 h após política de alteração; reaproveitar motor, sem catálogo paralelo."
+P1,AUD-SEARCH-T01,QA — revisão Tempo pendente,Revisão visual da Tempo; NLP separado.,"Filtro único, Mais filtros, preparação explícita, moeda obrigatória para montantes, pedidos canceláveis, estados e contexto únicos.",,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/catalogue,Revisão visual da Tempo; NLP separado.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T02,QA — revisão Tempo pendente,Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,"Sector básico, critérios especializados recolhidos em Mais filtros, estado de auditoria retirado do controlo público, contagem identificada e filtros removíveis.",,HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/business-marketplace,Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T03,QA — revisão Tempo pendente,Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,Contagem identificada como Sectores; navegação comum preserva a entrada por sector e Fintech como especialização.,,"Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação.",,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services,Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,"Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação.",,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T04,QA — revisão Tempo pendente,Nenhuma coordenada ou geometria criada.,Mesmos filtros e permissões; locais com coordenadas e oportunidades sem coordenadas apresentados separadamente; fichas de contexto identificadas.,,Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/catalogue,Nenhuma coordenada ou geometria criada.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T05,QA — revisão Tempo pendente,Provas anteriores de edição/publicação conservadas; não repetidas.,"Contagem Fichas da carteira, chips removíveis, menu local redundante retirado, Consultar ficha e Ver oportunidades associadas; retorno com query local.",,HTTP/Inertia de vazio; navegador de vazio; teste de contexto e navegação.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portfolio/index; portfolio/show,Provas anteriores de edição/publicação conservadas; não repetidas.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia de vazio; navegador de vazio; teste de contexto e navegação.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T06,QA — revisão Tempo pendente,Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha.,Destinos relacionados recolhidos; vazio neutro; consultar ficha separado de oportunidades associadas; retorno e exportação preservam filtros.,,HTTP/Inertia de vazio; inspecção da query/exportação e helpers de retorno.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/zones; investment/zone,Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia de vazio; inspecção da query/exportação e helpers de retorno.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T07,QA — revisão Tempo pendente,Nenhuma equivalência entre implantação e associação criada.,"Reutiliza carteira, contagem nomeada e tipo de ficha removível; URL anterior preservada.",,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portfolio/index,Nenhuma equivalência entre implantação e associação criada.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T08,QA — revisão Tempo pendente,Nenhuma alteração de relações ou dados.,Reutiliza carteira e ligações com finalidade explícita; URL anterior preservada.,,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portfolio/index,Nenhuma alteração de relações ou dados.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T09,QA — revisão Tempo pendente,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido.,,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/catalogue,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T10,QA — revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,"Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
+P1,AUD-SEARCH-T11,QA — revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,"Situação jurídica, consulta e cálculo continuam independentes.",,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
diff --git a/docs/investmoz/v2/auditoria-portal/backlog.md b/docs/investmoz/v2/auditoria-portal/backlog.md
index cc392eb0..06d3c259 100644
--- a/docs/investmoz/v2/auditoria-portal/backlog.md
+++ b/docs/investmoz/v2/auditoria-portal/backlog.md
@@ -2985,3 +2985,8 @@ Recuperação: parar a path unit, aguardar o executor e usar `python3 /var/www/d
As correcções do ensaio limitaram-se à composição: diagnóstico mais recente dos programadores, teste de exemplo sem migração inútil, fixtures unitárias independentes das flags do destino, expectativas dos menus/relatórios já aceites e guarda do cookie fora do navegador, com prova de CSRF preservado. Nenhuma nova regra organizacional.
Provas: outputs/investmoz-git-delivery-20261020/RECIBO-DISPONIBILIZACAO-GIT-MAIN.json; PROVA-ENSAIO-COMPLETO.json; PROVA-CONTROLO-FINAL-LINUX.json; PROVA-GITEA-EVENTO-E-INSTALACAO.json; PROVA-VISUAL-GIT-MAIN.png; deploy/homologacao/README.md; /build/revision.json. Manual versionado: `deploy/homologacao/README.md`. Esta entrega encerra o fluxo automático, não os restantes componentes da aplicação. As alterações documentais anteriores no checkout local foram preservadas.
+
+
+## Pesquisa transversal QA — 20/10/2026
+
+Inventário finito AUD-SEARCH-T01–T11 na mesma matriz/CSV e comparação no manual. Correcções manuais na branch `codex/opportunity-search`, sem integração/remoto/publicação. Assistência NLP ainda não implementada: interpretador e prévia/confirmar da mesma query manual, 6–10 h para recorte local delimitado; modelo externo sujeito a decisão. As oito lacunas contratuais e P7 mantêm-se separados. Revisão Tempo pendente; as provas comuns não encerram requisitos de gestão ou publicação não alterados.
diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv
index 957832c0..3ad6e7dc 100644
--- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv
+++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv
@@ -10639,3 +10639,14 @@ AUD-FINTECH-01,"Caracterizar e acompanhar investimento Fintech no mesmo dossier/
[RECONCILIACAO-43-20261019] Código e critérios confrontados com leitura READ ONLY fresca; PROVA-RECONCILIACAO-43-LEITURA.json, RECONCILIACAO-43-QA-FONTES.json, RECONCILIACAO-43-SCHEMA-QA.json. Não encerra critério funcional por inventário.
[PLANO-P1-P6-20261019] Plano Tempo: 27–46 h disponibilização; P7 e desenvolvimento separados. Inclusão 36–39 expressa para decisão; sem publicação automática. Revisão de proposta d87970e701f0. Roteiro no manual existente; não encerra critério por instalação.",Setores e Fintech — QA 15/10/2026,15/10/2026; preserva fechos e B.,,,,,,,,,,"Cenários sintéticos, não comprovam licença institucional/atividade financeira real ou correspondência jurídica específica; 12 critérios aceites não reabertos.",,,,
+AUD-SEARCH-T01,Consistência transversal da pesquisa e navegação — Oportunidades,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/oportunidades,"investment/catalogue — Filtro único, Mais filtros, preparação explícita, moeda obrigatória para montantes, pedidos canceláveis, estados e contexto únicos.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",QA — delta delimitado; revisão Tempo pendente,Revisão visual da Tempo; NLP separado.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Filtros duplicados, códigos internos, maturidades cruzadas, contagens concorrentes e mapa em grelha.",Revisão visual da Tempo; NLP separado.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,"6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado.",Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Revisão visual da Tempo; NLP separado.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T02,Consistência transversal da pesquisa e navegação — Negócios,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/negocios,"investment/business-marketplace — Sector básico, critérios especializados recolhidos em Mais filtros, estado de auditoria retirado do controlo público, contagem identificada e filtros removíveis.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados.,QA — delta delimitado; revisão Tempo pendente,Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Classificação técnica visível, estados de auditoria e contagem territorial sem âmbito.",Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,investment/business-marketplace; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T03,Consistência transversal da pesquisa e navegação — Sectores,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/sectores; /sectores/{slug},portal/services — Contagem identificada como Sectores; navegação comum preserva a entrada por sector e Fintech como especialização.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,"Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação.",QA — delta delimitado; revisão Tempo pendente,Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Navegação relacionada dispersa e contagens genéricas sem nome do universo.,Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,portal/services; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,"Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação.",Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T04,Consistência transversal da pesquisa e navegação — Localizações,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/oportunidades?view=map,investment/catalogue — Mesmos filtros e permissões; locais com coordenadas e oportunidades sem coordenadas apresentados separadamente; fichas de contexto identificadas.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas.,QA — delta delimitado; revisão Tempo pendente,Nenhuma coordenada ou geometria criada.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mapa e contagem de oportunidades podiam usar universos diferentes sem explicação.,Nenhuma coordenada ou geometria criada.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Nenhuma coordenada ou geometria criada.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T05,Consistência transversal da pesquisa e navegação — Carteira,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/carteira,"portfolio/index; portfolio/show — Contagem Fichas da carteira, chips removíveis, menu local redundante retirado, Consultar ficha e Ver oportunidades associadas; retorno com query local.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia de vazio; navegador de vazio; teste de contexto e navegação.,QA — delta delimitado; revisão Tempo pendente,Provas anteriores de edição/publicação conservadas; não repetidas.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Menu local repetido, ausência de retorno explícito e de distinção entre ficha e pesquisa associada.",Provas anteriores de edição/publicação conservadas; não repetidas.,portfolio/index; portfolio/show; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia de vazio; navegador de vazio; teste de contexto e navegação.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Provas anteriores de edição/publicação conservadas; não repetidas.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T06,Consistência transversal da pesquisa e navegação — Zonas e parques,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/zonas; /zonas/{id},investment/zones; investment/zone — Destinos relacionados recolhidos; vazio neutro; consultar ficha separado de oportunidades associadas; retorno e exportação preservam filtros.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia de vazio; inspecção da query/exportação e helpers de retorno.,QA — delta delimitado; revisão Tempo pendente,Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Destinos repetidos, vazio atribuído indevidamente a falta de cadastro, retorno sem filtros.",Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha.,investment/zones; investment/zone; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia de vazio; inspecção da query/exportação e helpers de retorno.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T07,Consistência transversal da pesquisa e navegação — Clusters,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/clusters → /carteira?kind=cluster,"portfolio/index — Reutiliza carteira, contagem nomeada e tipo de ficha removível; URL anterior preservada.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,QA — delta delimitado; revisão Tempo pendente,Nenhuma equivalência entre implantação e associação criada.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da carteira; não é um catálogo independente de oportunidades.,Nenhuma equivalência entre implantação e associação criada.,portfolio/index; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Nenhuma equivalência entre implantação e associação criada.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T08,Consistência transversal da pesquisa e navegação — Corredores,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/corredores → /carteira?kind=corridor,portfolio/index — Reutiliza carteira e ligações com finalidade explícita; URL anterior preservada.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,QA — delta delimitado; revisão Tempo pendente,Nenhuma alteração de relações ou dados.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da carteira e risco de confundir ficha territorial com oportunidade.,Nenhuma alteração de relações ou dados.,portfolio/index; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,Redireccionamento verificado no teste PHP; consumidor comum inspeccionado.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Nenhuma alteração de relações ou dados.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T09,Consistência transversal da pesquisa e navegação — Educação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/educacao,investment/catalogue — Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,QA — delta delimitado; revisão Tempo pendente,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Herda formulário e contagens concorrentes das oportunidades.,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,investment/catalogue; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T10,Consistência transversal da pesquisa e navegação — Biblioteca,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/documentos,"portal/services; portal/document — Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,QA — delta delimitado; revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,"Dois resumos de contagem, filtros activos sem remoção individual, controlos sem opções e rodapé local repetido.",Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
+AUD-SEARCH-T11,Consistência transversal da pesquisa e navegação — Legislação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/legislacao,portal/services; portal/document — Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,QA — delta delimitado; revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da biblioteca; não deve expor um universo concorrente.,"Situação jurídica, consulta e cálculo continuam independentes.",portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,"Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Situação jurídica, consulta e cálculo continuam independentes.",,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local.
diff --git a/resources/js/components/catalogue-filter-context.tsx b/resources/js/components/catalogue-filter-context.tsx
new file mode 100644
index 00000000..d761cea6
--- /dev/null
+++ b/resources/js/components/catalogue-filter-context.tsx
@@ -0,0 +1,140 @@
+import { Link, usePage } from '@inertiajs/react';
+
+const labels: Record = {
+ q: 'Pesquisa',
+ query: 'Pesquisa',
+ sector: 'Sector',
+ kind: 'Tipo de ficha',
+ modality: 'Modalidade',
+ category: 'Categoria',
+ physical_category: 'Tipo de espaço',
+ legal_regime: 'Regime',
+ development_status: 'Desenvolvimento',
+ availability_status: 'Disponibilidade',
+ province_id: 'Província',
+ district_id: 'Distrito',
+ admin_post_id: 'Posto administrativo',
+ locality_id: 'Localidade',
+ geo_location_id: 'Localização',
+ territory_id: 'Território',
+ province: 'Província',
+ activity: 'Actividade',
+ document_type: 'Tipo de documento',
+ topic: 'Tema',
+ issuer: 'Entidade emissora',
+ language: 'Idioma',
+ legal_status: 'Situação documental',
+ source_confirmation: 'Fonte',
+ stage: 'Etapa',
+ module: 'Área',
+ association_type: 'Associação',
+ association_target: 'Referência',
+ association_purpose: 'Relação',
+ association_state: 'Confirmação',
+ chain_id: 'Cadeia de valor',
+ subsector_id: 'Actividade',
+ authority_id: 'Entidade',
+};
+const territoryKeys = [
+ 'province_id',
+ 'district_id',
+ 'admin_post_id',
+ 'locality_id',
+];
+const values: Record = {
+ corridor: 'Corredores',
+ cluster: 'Clusters',
+ project: 'Projectos âncora',
+ zee: 'ZEE',
+ zfi: 'ZFI',
+ industrial_park: 'Parque industrial',
+ investment_project: 'Projecto de investimento',
+ company_sale: 'Venda de empresa',
+ company_purchase: 'Compra de empresa',
+ real_estate: 'Imobiliário',
+ land_right: 'Direitos fundiários',
+ turismo: 'Turismo',
+ agronegocio: 'Agronegócio',
+ fintech: 'Fintech — especialização do Mercado Financeiro',
+ 'mercado-financeiro': 'Mercado Financeiro',
+ legislation: 'Legislação',
+ procedure: 'Procedimento oficial',
+ official_form: 'Formulário oficial',
+ guide: 'Guia',
+ study: 'Estudo',
+ unknown: 'Por confirmar',
+ pt: 'Português',
+ en: 'Inglês',
+ confirmed: 'Confirmada',
+ pending: 'Por confirmar',
+};
+/** Name each active criterion without exposing IDs or taxonomy codes. */
+export default function CatalogueFilterContext() {
+ const page = usePage();
+ const filters = page.props.filters as Record | undefined;
+ if (!filters) return null;
+ const active = Object.entries(filters).filter(
+ ([key, value]) =>
+ !['page', 'per_page', 'sort', 'view', 'publication_state'].includes(
+ key,
+ ) &&
+ value !== '' &&
+ value != null &&
+ value !== false &&
+ (!Array.isArray(value) || value.length),
+ );
+ if (!active.length) return null;
+ const describe = (key: string, value: unknown): string => {
+ const path = page.props.territorySelection as
+ | { id: string; name: string }[]
+ | undefined;
+ const place = path?.find((item) => item.id === value)?.name;
+ if (place) return place;
+ if (typeof value !== 'string') return 'seleccionado';
+ return (
+ values[value] ??
+ (['q', 'query', 'province', 'topic', 'issuer'].includes(key)
+ ? value
+ : 'seleccionado')
+ );
+ };
+ const href = (remove?: string) => {
+ const url = new URL(page.url, 'https://portal.invalid');
+ const keys = remove
+ ? [
+ remove,
+ ...territoryKeys
+ .slice(territoryKeys.indexOf(remove) + 1)
+ .filter(() => territoryKeys.includes(remove)),
+ ]
+ : Object.keys(filters);
+ for (const key of keys) {
+ url.searchParams.delete(key);
+ url.searchParams.delete(`${key}[]`);
+ }
+ url.searchParams.delete('page');
+ return url.pathname + url.search;
+ };
+ return (
+
+ );
+}
diff --git a/resources/js/components/catalogue-navigation.tsx b/resources/js/components/catalogue-navigation.tsx
index c6fea856..94e82a24 100644
--- a/resources/js/components/catalogue-navigation.tsx
+++ b/resources/js/components/catalogue-navigation.tsx
@@ -73,34 +73,32 @@ export default function CatalogueNavigation({
],
};
return (
-
+
+
+ Catálogos relacionados
+
+
+
);
}
diff --git a/resources/js/components/country-select.tsx b/resources/js/components/country-select.tsx
index e21e7616..b794c9b9 100644
--- a/resources/js/components/country-select.tsx
+++ b/resources/js/components/country-select.tsx
@@ -50,7 +50,14 @@ export default function CountrySelect({
onChange={(event) => setSearch(event.target.value)}
placeholder="Pesquisar nome ou código do país"
/>
- {search.trim() &&
{matches.length ? `${matches.length} resultados encontrados` : "Não foram encontrados resultados para esta pesquisa"}
- {loading ? 'A pesquisar…' : error ? <>Não foi possível concluir a pesquisa. > : total === 0 ? 'Não foram encontrados resultados para esta pesquisa' : `${total} resultados encontrados`}
-
- Combine contexto territorial, dimensão e necessidades. As fichas
- documentais e as oportunidades mantêm a sua origem e natureza.
+ Combine os filtros para encontrar oportunidades publicadas.
+ Nenhum critério é alargado automaticamente.
-
- Negócios existentes, participações, imóveis e activos só surgem
- como anúncio depois de revisão e publicação expressa. A pesquisa
- de negócios é um catálogo separado; ausência de resultados não
- confirma inexistência do activo. As referências governamentais
- da carteira são separadas de anúncios privados.
-
- O mapa atingiu o limite de 1.000 registos; refine os
- filtros. A cobertura não está completa.
-
- )}
- {unlocated.length > 0 && (
-
-
- {unlocated.length} investimentos sem coordenadas de
- local confirmadas
-
-
- Continuam nos resultados. Seleccionar uma província não
- determina a posição exacta do investimento.
+ {!searchLoading &&
+ !searchError &&
+ ['map', 'combined'].includes(view) &&
+ truncated && (
+
+ O mapa atingiu o limite de 1.000 registos; refine os
+ filtros. A cobertura não está completa.
- Esta ficha não regista data de validação
- documental. Confirme a origem e as condições
- antes de agir.
+ )}
+ {!searchLoading &&
+ !searchError &&
+ view === 'map' &&
+ unlocated.length > 0 && (
+
+
+ {unlocated.length} investimentos sem coordenadas de
+ local confirmadas
+
+
+ Continuam nos resultados. Seleccionar uma província
+ não determina a posição exacta do investimento.
+
{zone.resources?.length ? `${zone.resources.length} recursos descritos; consulte o âmbito e a fonte na ficha.` : 'Infraestruturas, serviços, acessos e capacidades: não informados.'} {!zone.activities?.length && 'Atividades: não informadas.'}
}
+ {zone.category === 'industrial_park' && (
+
+ {zone.resources?.length
+ ? `${zone.resources.length} recursos descritos; consulte o âmbito e a fonte na ficha.`
+ : 'Infraestruturas, serviços, acessos e capacidades: não informados.'}{' '}
+ {!zone.activities?.length &&
+ 'Atividades: não informadas.'}
+