diff --git a/app/Http/Controllers/GlobalSearchController.php b/app/Http/Controllers/GlobalSearchController.php new file mode 100644 index 00000000..ca7e97b5 --- /dev/null +++ b/app/Http/Controllers/GlobalSearchController.php @@ -0,0 +1,173 @@ +validate(['q' => ['nullable', 'string', 'max:120'], 'page' => ['nullable', 'integer', 'min:1']]); + $term = trim($input['q'] ?? ''); + $tokens = $search->tokens($term); + $results = ['data' => [], 'total' => 0, 'current_page' => 1, 'last_page' => 1, 'per_page' => 5, 'unavailable' => []]; + if ($tokens !== []) { + $factories = [ + 'Páginas e serviços' => function () use ($search, $tokens, $term) { + $pages = array_values(array_filter($this->pages(), fn ($page) => $search->matches(implode(' ', $page), $tokens))); + usort($pages, fn ($a, $b) => $search->score($b['title'], $term, $tokens) <=> $search->score($a['title'], $term, $tokens) + ?: strcmp($a['title'], $b['title']) ?: strcmp($a['url'], $b['url'])); + + return ['count' => count($pages), 'fetch' => fn ($offset, $limit) => array_slice($pages, $offset, $limit)]; + }, + 'Oportunidades' => function () use ($catalogue, $search, $tokens, $term) { + $query = $catalogue->query(['include_national' => true]); + foreach ($tokens as $token) { + $query->where(function ($match) use ($catalogue, $search, $token) { + $search->orFields($match, ['opportunities.title', 'opportunities.summary', 'opportunities.description', 'opportunities.reference', 'opportunities.province'], $token); + $match->orWhereHas('sector', fn ($sector) => $search->constrain($sector, ['name'], [$token])); + $match->orWhereExists(fn ($location) => $location->selectRaw('1')->from('geo_locations as search_location') + ->whereIn('search_location.id', app(InvestmentGeography::class)->publicReferences()->select('id')) + ->where(fn ($name) => $search->orFields($name, ['search_location.name'], $token)) + ->where(fn ($direct) => $direct->whereColumn('search_location.id', 'opportunities.primary_territory_id') + ->orWhereExists(fn ($link) => $link->selectRaw('1')->from('opportunity_locations as search_link') + ->whereColumn('search_link.opportunity_id', 'opportunities.id')->whereColumn('search_link.geo_location_id', 'search_location.id')))); + if (preg_match('/^[a-z0-9]+$/', $token)) { + $match->orWhereIn('opportunities.id', $catalogue->query(['q' => $token, 'include_national' => true])->reorder()->select('opportunities.id')); + } + }); + } + + return $this->source($query, $search, $term, $tokens, 'opportunities.title', 'opportunities.id', + fn ($r) => ['title' => $r->title, 'description' => $r->summary, 'url' => '/oportunidades/'.rawurlencode($r->slug)]); + }, + 'Negócios' => function () use ($search, $tokens, $term) { + $query = ExistingBusiness::query()->published(); + $search->constrain($query, ['public_title', 'activity_code', 'objective'], $tokens); + + return $this->source($query, $search, $term, $tokens, 'public_title', 'existing_businesses.id', + fn ($r) => ['title' => $r->public_title, 'description' => $r->objective, 'url' => '/negocios/'.rawurlencode($r->public_slug)]); + }, + 'Carteira: projectos, clusters e corredores' => function () use ($search, $tokens, $term) { + $query = InvestmentPortfolioController::visible(); + $search->constrain($query, ['investment_dossiers.title', 'investment_dossiers.content'], $tokens); + + return $this->source($query, $search, $term, $tokens, 'investment_dossiers.title', 'investment_dossiers.id', + fn ($r) => ['title' => $r->title, 'description' => $r->corridor_id ? 'Corredor' : ($r->cluster_id ? 'Cluster' : 'Projecto âncora'), 'url' => '/carteira/'.$r->id]); + }, + 'Documentos e legislação' => function () use ($search, $tokens, $term) { + $query = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id') + ->where('r.published', true)->where('r.rights_status', '!=', 'restricted')->where('d.rights_status', '!=', 'restricted') + ->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at') + ->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256'); + $search->constrain($query, ['r.title', 'r.description', 'r.document_number', 'd.issuer', 'r.source_reference', 'r.category', 'r.topic'], $tokens); + $query->select('r.id', 'r.title', 'r.description'); + + return $this->source($query, $search, $term, $tokens, 'r.title', 'r.id', + fn ($r) => ['title' => $r->title, 'description' => $r->description, 'url' => '/documentos/'.$r->id]); + }, + 'Artigos' => function () use ($search, $tokens, $term) { + if (! Schema::hasColumn('portal_resources', 'editorial_content')) { + return ['count' => 0, 'fetch' => fn ($offset, $limit) => []]; + } + $query = app(EditorialReading::class)->publicResources() + ->whereRaw("nullif(btrim(r.editorial_content->>'body'), '') is not null"); + $search->constrain($query, ['r.title', "(r.editorial_content->>'body')"], $tokens); + $query->select('r.id', 'r.title')->selectRaw("r.editorial_content->>'body' as body"); + + return $this->source($query, $search, $term, $tokens, 'r.title', 'r.id', + fn ($r) => ['title' => $r->title, 'description' => Str::limit(trim(strip_tags($r->body)), 220), 'url' => '/artigos/'.$r->id]); + }, + 'Sectores' => function () use ($search, $tokens, $term) { + $query = Sector::query()->where('is_active', true)->whereNotNull('published_at'); + $search->constrain($query, ['name', 'description', 'short_description'], $tokens); + + return $this->source($query, $search, $term, $tokens, 'name', 'sectors.id', + fn ($r) => ['title' => $r->name, 'description' => $r->short_description, 'url' => SectorOrganization::href($r->slug)]); + }, + 'Localizações' => function () use ($search, $tokens, $term) { + $query = app(InvestmentGeography::class)->publicReferences() + ->whereIn('location_type', ['province', 'district', 'admin_post', 'locality']); + $search->constrain($query, ['geo_locations.name', 'geo_locations.source_code'], $tokens); + $query->select('geo_locations.id', 'geo_locations.name', 'geo_locations.location_type'); + + return $this->source($query, $search, $term, $tokens, 'geo_locations.name', 'geo_locations.id', + fn ($r) => ['title' => $r->name, 'description' => ['province' => 'Província', 'district' => 'Distrito', 'admin_post' => 'Posto administrativo', 'locality' => 'Localidade'][$r->location_type], + 'url' => '/oportunidades?'.http_build_query([$r->location_type.'_id' => $r->id])]); + }, + 'Zonas de investimento' => function () use ($search, $tokens, $term) { + $query = app(InvestmentZones::class)->visible(); + $search->constrain($query, ['name', 'code'], $tokens); + + return $this->source($query, $search, $term, $tokens, 'name', 'investment_zones.id', + fn ($r) => ['title' => $r->name, 'description' => $r->code, 'url' => '/zonas/'.$r->id]); + }, + 'Parcerias' => function () use ($search, $tokens, $term) { + $query = DB::table('portal_partnerships as p')->where('p.status', 'published'); + $search->constrain($query, ['p.title', 'p.sector', 'p.province'], $tokens); + $query->select('p.id', 'p.title', 'p.sector', 'p.province'); + + return $this->source($query, $search, $term, $tokens, 'p.title', 'p.id', + fn ($r) => ['title' => $r->title, 'description' => implode(' · ', array_filter([$r->sector, $r->province])), 'url' => '/parcerias/'.$r->id]); + }, + ]; + $results = $federated->page($factories, (int) ($input['page'] ?? 1), 5, + fn ($item) => $search->score($item['title'], $term, $tokens)); + } + + return Inertia::render('portal/search', ['query' => $term, 'results' => $results]); + } + + private function source($query, SearchTerms $search, string $term, array $tokens, string $title, string $id, callable $present): array + { + $count = (clone $query)->reorder()->count(); + $query->reorder(); + $search->orderByRelevance($query, $title, $term, $tokens, $id); + + return ['count' => $count, 'fetch' => fn ($offset, $limit) => (clone $query)->offset($offset)->limit($limit)->get()->map($present)->all()]; + } + + /** Public service entry points and terms used to find them. */ + private function pages(): array + { + return [ + ['title' => 'Como investir em Moçambique', 'description' => 'Procedimentos e etapas para investir', 'url' => '/como-investir'], + ['title' => 'Iniciar ou continuar investimento', 'description' => 'Preparar uma ideia e dossier de investimento', 'url' => '/investimentos/iniciar'], + ['title' => 'Oportunidades de investimento', 'description' => 'Pesquisar por sector, localização e maturidade', 'url' => '/oportunidades'], + ['title' => 'Carteira de investimento', 'description' => 'Projectos âncora, clusters e corredores', 'url' => '/carteira'], + ['title' => 'Negócios existentes', 'description' => 'Compra, venda e participação em negócios', 'url' => '/negocios'], + ['title' => 'Sectores de investimento', 'description' => 'Actividades económicas e sectores prioritários', 'url' => '/sectores'], + ['title' => 'Zonas de investimento', 'description' => 'Zonas económicas especiais, ZEE, ZFI e parques industriais', 'url' => '/zonas'], + ['title' => 'Contextos de investimento', 'description' => 'Formas de iniciar e preparar um investimento', 'url' => '/contextos-de-investimento'], + ['title' => 'Documentos e formulários', 'description' => 'Acervo documental e formulários para investidores e empresas', 'url' => '/documentos'], + ['title' => 'Formulário do investidor', 'description' => 'Dados e contacto para investir', 'url' => '/formularios/investidor'], + ['title' => 'Formulário de empresa', 'description' => 'Dados e contacto para empresas', 'url' => '/formularios/empresa'], + ['title' => 'Legislação e incentivos', 'description' => 'Leis, decretos, benefícios e enquadramento legal', 'url' => '/legislacao'], + ['title' => 'Simulador fiscal', 'description' => 'Simular impostos e cenários fiscais de investimento', 'url' => '/simulador-fiscal'], + ['title' => 'Formulários fiscais', 'description' => 'Preparação dos formulários IVA e IRPC', 'url' => '/formularios/fiscais'], + ['title' => 'Terra e DUAT', 'description' => 'Apoio ao uso e aproveitamento da terra', 'url' => '/terra-duat'], + ['title' => 'Bolsa de parcerias', 'description' => 'Encontrar parceiros e propostas', 'url' => '/parcerias'], + ['title' => 'Apoio ao investidor', 'description' => 'Contactos e pedido de apoio', 'url' => '/apoio'], + ['title' => 'Educação para investimento', 'description' => 'Conteúdos educativos e orientação', 'url' => '/educacao'], + ['title' => 'Sobre o InvesteMoz', 'description' => 'Informação institucional do portal', 'url' => '/sobre'], + ['title' => 'Contacto', 'description' => 'Contactar o portal InvesteMoz', 'url' => '/contacto'], + ['title' => 'Política de privacidade', 'description' => 'Privacidade e tratamento de dados pessoais', 'url' => '/politica-de-privacidade'], + ['title' => 'Termos de uso', 'description' => 'Condições de utilização do portal', 'url' => '/termos-de-uso'], + ]; + } +} diff --git a/app/Services/FederatedSearch.php b/app/Services/FederatedSearch.php new file mode 100644 index 00000000..dfeb49b3 --- /dev/null +++ b/app/Services/FederatedSearch.php @@ -0,0 +1,78 @@ + $factories */ + public function page(array $factories, int $requestedPage, int $size = 5, ?callable $score = null): array + { + $sources = []; + $unavailable = []; + foreach ($factories as $label => $factory) { + try { + $sources[$label] = $factory(); + } catch (QueryException $exception) { + report($exception); + $unavailable[] = $label; + } + } + + // If a source fails after its count, remove it and recompute the page offsets. + do { + $failed = false; + $total = array_sum(array_column($sources, 'count')); + $last = max(1, (int) ceil($total / $size)); + $page = min(max(1, $requestedPage), $last); + $first = ($page - 1) * $size; + $items = []; + $positions = array_fill_keys(array_keys($sources), 0); + $buffers = array_fill_keys(array_keys($sources), []); + for ($position = 0; $position < min($total, $first + $size); $position++) { + $best = null; + $bestScore = PHP_INT_MIN; + foreach ($sources as $label => $source) { + if ($buffers[$label] === [] && $positions[$label] < $source['count']) { + try { + $batch = iterator_to_array((function () use ($source, $positions, $label, $size) { + yield from ($source['fetch'])($positions[$label], $size); + })(), false); + $positions[$label] += count($batch); + $buffers[$label] = $batch; + if ($batch === []) { + $positions[$label] = $source['count']; + } + } catch (QueryException $exception) { + report($exception); + $unavailable[] = $label; + unset($sources[$label]); + $failed = true; + break; + } + } + if ($buffers[$label] === []) { + continue; + } + $candidateScore = $score ? $score($buffers[$label][0]) : 0; + if ($best === null || $candidateScore > $bestScore) { + $best = $label; + $bestScore = $candidateScore; + } + } + if ($failed || $best === null) { + break; + } + $item = array_shift($buffers[$best]); + if ($position >= $first) { + $items[] = [...$item, 'category' => $best]; + } + } + } while ($failed); + + return ['data' => $items, 'total' => $total, 'current_page' => $page, 'last_page' => $last, + 'per_page' => $size, 'unavailable' => $unavailable]; + } +} diff --git a/app/Services/SearchTerms.php b/app/Services/SearchTerms.php new file mode 100644 index 00000000..48c1bf0c --- /dev/null +++ b/app/Services/SearchTerms.php @@ -0,0 +1,112 @@ + */ + public function tokens(string $query): array + { + $normalized = mb_strtolower(Str::ascii($query)); + $tokens = array_values(array_filter(array_unique(array_map( + fn ($word) => trim($word, ".,;:!?\"'()[]{}"), + preg_split('/\s+/u', trim($normalized), -1, PREG_SPLIT_NO_EMPTY) ?: [] + )), fn ($word) => $word !== '')); + if (count($tokens) > 1) { + $meaningful = array_values(array_diff($tokens, ['a', 'as', 'o', 'os', 'de', 'do', 'da', 'dos', 'das', 'em', 'no', 'na', 'nos', 'nas', 'para', 'por', 'e', 'como', 'qual', 'quais', 'um', 'uma'])); + + return $meaningful === [] ? $tokens : $meaningful; + } + + return $tokens; + } + + /** Known Portuguese spelling and number variants; do not infer semantic equivalence. */ + public function variants(string $token): array + { + foreach ([['sector', 'sectores', 'setor', 'setores'], ['projecto', 'projectos', 'projeto', 'projetos'], + ['investir', 'investimento', 'investimentos'], ['investidor', 'investidores'], ['oportunidade', 'oportunidades'], ['documento', 'documentos'], + ['empresa', 'empresas'], ['parceria', 'parcerias'], ['zona', 'zonas'], ['formulario', 'formularios'], + ['provincia', 'provincias'], ['corredor', 'corredores'], ['cluster', 'clusters'], ['imposto', 'impostos'], ['licenca', 'licencas']] as $group) { + if (in_array($token, $group, true)) { + return $group; + } + } + + return [$token]; + } + + public function pattern(string $token): string + { + return '%'.str_replace(['!', '%', '_'], ['!!', '!%', '!_'], $token).'%'; + } + + /** Columns are trusted application constants, never user input. */ + public function orFields($query, array $columns, string $token): void + { + foreach ($columns as $column) { + foreach ($this->variants($token) as $variant) { + $query->orWhereRaw($this->normalizedColumn($column)." LIKE ? ESCAPE '!'", [$this->pattern($variant)]); + } + } + } + + public function constrain($query, array $columns, array $tokens): void + { + foreach ($tokens as $token) { + $query->where(fn ($match) => $this->orFields($match, $columns, $token)); + } + } + + public function matches(string $text, array $tokens): bool + { + $text = mb_strtolower(Str::ascii($text)); + foreach ($tokens as $token) { + if (! array_filter($this->variants($token), fn ($variant) => str_contains($text, $variant))) { + return false; + } + } + + return true; + } + + private function normalizedColumn(string $column): string + { + return "translate(lower(coalesce({$column}::text, '')), 'áàâãäéèêëíìîïóòôõöúùûüçñ', 'aaaaaeeeeiiiiooooouuuucn')"; + } + + public function score(string $title, string $phrase, array $tokens): int + { + $title = mb_strtolower(Str::ascii($title)); + $phrase = mb_strtolower(Str::ascii(trim($phrase))); + $score = $title === $phrase ? 1000 : (str_contains($title, $phrase) ? 400 : 0); + foreach ($tokens as $token) { + if ($this->matches($title, [$token])) { + $score += 20; + } + } + + return $score; + } + + /** The same title score used for in-memory pages, with bound search terms. */ + public function orderByRelevance($query, string $titleColumn, string $phrase, array $tokens, string $tieColumn): void + { + $column = $this->normalizedColumn($titleColumn); + $phrase = mb_strtolower(Str::ascii(trim($phrase))); + $sql = "CASE WHEN {$column} = ? THEN 1000 WHEN {$column} LIKE ? ESCAPE '!' THEN 400 ELSE 0 END"; + $bindings = [$phrase, $this->pattern($phrase)]; + foreach ($tokens as $token) { + $conditions = []; + foreach ($this->variants($token) as $variant) { + $conditions[] = "{$column} LIKE ? ESCAPE '!'"; + $bindings[] = $this->pattern($variant); + } + $sql .= ' + CASE WHEN ('.implode(' OR ', $conditions).') THEN 20 ELSE 0 END'; + } + $query->orderByRaw('('.$sql.') DESC', $bindings)->orderBy($titleColumn)->orderBy($tieColumn); + } +} diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index a6f2752c..87eb39bb 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -7384,3 +7384,20 @@ Estado actual: `investor-usability-git-delivery-20261028.json`. Os relatórios Q Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir legislação. Base preservada: entrega instalada 789fffd0, Modelo A e aquisições/apuramento já aceites, L6 M20/M22 instalado em 5aaf1a56 e incrementos de L8. Nenhuma perda confirmada neste recorte; não se apagam as aceitações nem se repetem provas fiscais anteriores. Fontes reutilizadas do inventário de 20 exemplares acessíveis em outputs/irpc-l6-values-forms/accessible-source-inventory.json da QA anterior, incluindo temporários identificados; inventário do corpus e originais preservados. M22: três páginas conferidas visualmente nesta revisão, SHA256 4b61bd83d1fbcfd3b7b5c5523cea5923f7d0f65dc4c004eff58e5db38f763c88. Modelo A: correspondência e divergências aceites em modelo-a-reconciliacao-20261021.md; sem nova validação de proveniência. Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação. + + +### 03/11/2026 — pesquisa rápida, incremento local em preparação + +Inventário reutilizado: a pesquisa da página inicial encaminhava ao catálogo de oportunidades; continuam disponíveis os catálogos de oportunidades, carteira, documentos, sectores, zonas e negócios, com regras próprias de publicação. Nenhuma perda foi demonstrada nesta comparação de código. A alteração local encaminha para uma página agrupada com estes seis domínios e paginação por grupo, reutilizando os filtros públicos dos destinos; não altera dados nem permissões. O incremento ainda não comprova pesquisa integral: faltam outros módulos, pesquisa de conteúdo dos ficheiros, correspondência mais flexível e prova funcional com dados na base QA isolada. A pesquisa vazia e a validação de comprimento passaram; a prova com dados ficou pendente por falta de credenciais da base QA. Não instalado no activo. A referência ao pedido de pesquisa na matriz existente (MASTER-9dee08d0-0016 e MASTER-471012ab-0124) não altera a aceitação anterior nem encerra estes requisitos. + + +### 03/11/2026 — pesquisa sem acesso à base + +A pedido da Tempo, continuação por análise do código e testes isolados, sem ligar à base. A pesquisa local passou a combinar palavras em qualquer ordem, ignorar maiúsculas e acentos portugueses nos campos pesquisados e tratar % e _ literalmente. Agrupa páginas/serviços, oportunidades, negócios, carteira, documentos/legislação, sectores, zonas e parcerias públicas; preserva os filtros de visibilidade dos percursos e sinaliza fontes indisponíveis. O catálogo estático de serviços pode ser pesquisado sem consultar tabelas; o acesso HTTP ao portal continua dependente da sua configuração. Documentos são pesquisados pelos metadados, não pelo conteúdo dos ficheiros; outros módulos e dados privados não estão cobertos. Testes isolados não demonstram resultados nem desempenho contra dados reais ou o funcionamento de toda a aplicação sem base. Sem instalação, migração, publicação ou alteração de dados no activo. Referência na matriz existente: MASTER-9dee08d0-0016, com aceitação anterior preservada. + + +### 04/11/2026 — pesquisa unificada, candidata local + +A pedido da Tempo, mantida a execução sem acesso à base e preservados os catálogos e provas anteriores. A pesquisa passa a apresentar uma sequência global de cinco resultados por página, números de página e a expressão efectiva, com cores vermelho, preto e branco; as fontes públicas são combinadas por relevância do título e identificadas por tipo. Aceita algumas variantes portuguesas de escrita, singular/plural e termos de ligação; acrescenta páginas públicas, localizações públicas e o corpo de edições editoriais publicadas e autorizadas. Contagens e blocos são obtidos por fonte, sem carregar integralmente os catálogos; uma fonte indisponível é identificada e as restantes mantidas. Escolhas técnicas de pesquisa, não novas regras organizacionais. + +Foram efectuados testes isolados de correspondência, sequência e paginação entre fontes, indisponibilidade, renderização e compilação; não há demonstração com dados reais ou de desempenho com volumes de produção. A pesquisa não indexa o texto dos ficheiros originais nem cobre todos os módulos e dados privados autorizados; não declarar pesquisa universal. Sem alteração de dados, permissões, ficheiros originais ou instalação no activo. Cobertura e limites registados em MASTER-9dee08d0-0016 na matriz existente, sem alterar aceitações anteriores. diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index 161429c5..88364bfa 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -407,7 +407,7 @@ MASTER-9dee08d0-0012,"Carteira PII 2026-2030: cada sector deve apresentar oportu MASTER-9dee08d0-0013,"Para transformar o portal num balcão único interoperável, as integrações de sistema devem ser tratadas como uma ambição funcional dependente de acordos institucionais, disponibilidade técnica das entidades públicas e validação jurídica dos fluxos de dados.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 161,Balcão/orientação,/balcao-virtual/contextos; /balcao-virtual/dossiers; /balcao-virtual/dossiers/{id}; /balcao-virtual/dossiers/{id}/documentos; /balcao-virtual/dossiers/{id}/documentos/{document}; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /balcao-virtual/dossiers/{id}/etapas; /balcao-virtual/dossiers/{id}/fontes; /balcao-virtual/dossiers/{id}/orientacao; /balcao-virtual/dossiers/{id}/pedidos; /balcao-virtual/dossiers/{id}/pressupostos; /balcao-virtual/dossiers/{id}/recomendacoes,"VirtualDesk, pedidos/casos, anexos, contexto dossier e acompanhamento. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,"Pedido contextual→gravar→submeter→nova sessão→gestor→mensagem/documentos/resultado, reaproveitando A/B. Critério individual original preservado; cobertura atual ainda não individualizada.","Perfis próprios do Balcão; serviços/campos e decisões externas aplicáveis, sem inventar destinatário/prazo. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","MASTER-9dee08d0-0013: Para transformar o portal num balcão único interoperável, as integrações de sistema devem ser tratadas como uma ambição funcional dependente de acordos institucionais, disponibilidade técnica das entidades públicas e validação jurídica dos fluxos de dados.",P2,MASTER-9dee08d0-0013: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0013,1,"[{""id"":""MASTER-9dee08d0-0013"",""pedido"":""Para transformar o portal num balcão único interoperável, as integrações de sistema devem ser tratadas como uma ambição funcional dependente de acordos institucionais, disponibilidade técnica das entidades públicas e validação jurídica dos fluxos de dados."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 161""],""aceitacao"":""Para transformar o portal num balcão único interoperável, as integrações de sistema devem ser tratadas como uma ambição funcional dependente de acordos institucionais, disponibilidade técnica das entidades públicas e validação jurídica dos fluxos de dados."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""Balcão/orientação""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.desk_cases: 1 registos; public.desk_requests: 0 registos; public.desk_institutional_links: 0 registos; public.desk_messages: 0 registos,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.","Pedido contextual→gravar→submeter→nova sessão→gestor→mensagem/documentos/resultado, reaproveitando A/B. Critério individual original preservado; cobertura atual ainda não individualizada.","VirtualDesk, pedidos/casos, anexos, contexto dossier e acompanhamento","Perfis próprios do Balcão; serviços/campos e decisões externas aplicáveis, sem inventar destinatário/prazo.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: balcão. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,balcão,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Services/VirtualDesk.php; app/Services/Ai/InvestorGuidance.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,InvestorGuidanceTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, MASTER-9dee08d0-0014,O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:,portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 186 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 177 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 190 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 194,governação/outros,/api/v1/investment/admin/interests/{interest}/status; /management/history; /management/interests; /management/interests/{interest}; /management/interests/{interest}/consent/contact; /management/interests/{interest}/consent/proponent; /management/opportunities; /management/opportunities/create; /management/opportunities/drafts; /management/opportunities/{id}/draft; /management/opportunities/{id}/edit; /management/opportunities/{id}/lifecycle,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.",MASTER-9dee08d0-0014: O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique: | MASTER-efd550fd-0014: O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique: | MASTER-9f5719c8-0015: O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique: | MASTER-626cae3d-0012: O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:,P2,MASTER-9dee08d0-0014: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0014: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0015: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0012: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0014 | MASTER-efd550fd-0014 | MASTER-9f5719c8-0015 | MASTER-626cae3d-0012,4,"[{""id"":""MASTER-9dee08d0-0014"",""pedido"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 186""],""aceitacao"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-efd550fd-0014"",""pedido"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 177""],""aceitacao"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-9f5719c8-0015"",""pedido"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 190""],""aceitacao"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""},{""id"":""MASTER-626cae3d-0012"",""pedido"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 194""],""aceitacao"":""O conteúdo e os fluxos de trabalho do portal devem refletir rigorosamente o quadro legal vigente em Moçambique:"",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""governação/outros""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,Requisito transversal; consultar estado-ativo-after.json,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.",Localizar obrigação verificável e consumidores/contrato atuais antes de estimar implementação ou declarar conclusão. Critério individual original preservado; cobertura atual ainda não individualizada.,"Entidades, schema/configuração e serviços existentes; correspondência individual por estabelecer","Ler secção/versão correspondente; distinguir requisito funcional, decisão e exemplo. Fontes oficiais já aceites.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: governação e demais cláusulas. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,governação e demais cláusulas,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",AGENTS.md — componentes do domínio encontrados; correspondência integral deste requisito não presumida,; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.,Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, MASTER-9dee08d0-0015,"Instruções claras sobre a obrigatoriedade de registo do capital de entrada junto do Banco de Moçambique para assegurar o direito de transferência de lucros, dividendos e capital investido de volta ao país de origem.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 191,dashboard/perfis,/api/v1/investment/companies/{id?}; /api/v1/investment/notifications; /api/v1/investment/notifications/{id}/read; /api/v1/investment/profile; /api/v1/investment/submissions/{id}/messages; /api/v1/investment/submissions/{id}/revisions/{version}; /dashboard; /investidor/companies/{id?}; /investidor/manifestacoes/{id}/messages; /investidor/manifestacoes/{id}/revisions/{version}; /investidor/notifications; /investidor/notifications/{id}/read,"Fortify, PortalProfileAdministration/Access, grants, policies, portal_security_approvals e menus atuais. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,"Reutilizar provas existentes; suplementar autorização/revogação/elevação por ação/consumidor, sem sistema paralelo. Critério individual original preservado; cobertura atual ainda não individualizada.","030000→040000 instalados, seis categorias ON; contas/grants reais concluídos. Autorizações por tipo/org e independência mantidas. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","MASTER-9dee08d0-0015: Instruções claras sobre a obrigatoriedade de registo do capital de entrada junto do Banco de Moçambique para assegurar o direito de transferência de lucros, dividendos e capital investido de volta ao país de origem.",P1,MASTER-9dee08d0-0015: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0015,1,"[{""id"":""MASTER-9dee08d0-0015"",""pedido"":""Instruções claras sobre a obrigatoriedade de registo do capital de entrada junto do Banco de Moçambique para assegurar o direito de transferência de lucros, dividendos e capital investido de volta ao país de origem."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 191""],""aceitacao"":""Instruções claras sobre a obrigatoriedade de registo do capital de entrada junto do Banco de Moçambique para assegurar o direito de transferência de lucros, dividendos e capital investido de volta ao país de origem."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""dashboard/perfis""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.investment_accounts: 0 registos; public.investment_company_declarations: 0 registos; public.investment_submissions: 3 registos,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global.","Reutilizar provas existentes; suplementar autorização/revogação/elevação por ação/consumidor, sem sistema paralelo. Critério individual original preservado; cobertura atual ainda não individualizada.","Fortify, PortalProfileAdministration/Access, grants, policies, portal_security_approvals e menus atuais","030000→040000 instalados, seis categorias ON; contas/grants reais concluídos. Autorizações por tipo/org e independência mantidas.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: perfis. Esforço de código adicional só após lacuna reproduzida.,outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,perfis,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Http/Controllers/DashboardController.php; app/Services/CountryCatalogue.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"InvestmentWorkspaceTest, InvestorGuidanceTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, -MASTER-9dee08d0-0016,"Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 208 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 195 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 208 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 213,simuladores,/api/v1/questionnaires/{code}; /api/v1/simulation-drafts; /api/v1/simulation-drafts/claim; /api/v1/simulation-drafts/claim-preview; /api/v1/simulation-drafts/current; /api/v1/simulation-drafts/linkable-businesses; /api/v1/simulation-drafts/{id}; /api/v1/simulation-drafts/{id}/business-link; /api/v1/simulation-drafts/{id}/conversions; /api/v1/simulation-drafts/{id}/touch; /api/v1/simulations; /api/v1/simulations/history,"SimulationExecution, resultados/cenários/fontes, rascunhos e dossier existentes. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Cenário→cálculo→gravação→resultado/dossier correto→nova sessão→PDF/derivadas autorizadas. Sem aprovação/score inventado. Critério individual original preservado; cobertura atual ainda não individualizada.,"Regras financeiras/fiscais separadas; fonte oficial aceite, parâmetros/âmbito/período podem faltar; não substituir por0/taxa atual. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","MASTER-9dee08d0-0016: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania. | MASTER-efd550fd-0016: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania. | MASTER-9f5719c8-0017: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania. | MASTER-626cae3d-0014: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania.",P2,MASTER-9dee08d0-0016: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0016: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0017: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0014: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0016 | MASTER-efd550fd-0016 | MASTER-9f5719c8-0017 | MASTER-626cae3d-0014,4,"[{""id"":""MASTER-9dee08d0-0016"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 208""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""},{""id"":""MASTER-efd550fd-0016"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 195""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""},{""id"":""MASTER-9f5719c8-0017"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 208""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""},{""id"":""MASTER-626cae3d-0014"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 213""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.simulation_form_drafts: 22 registos; public.simulation_scenarios: 16 registos; public.simulations: 16 registos; public.simulation_tax_rules: tabela ausente,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global. | 23/10/2026 — adequação do hero por pedido expresso da Tempo. Aplicadas as orientações permanentes AGENTS.md de 12/10 e 22/10/2026: regras da Tempo, escolhas técnicas identificadas e preservação de dados/histórico. Rotação, WebP e composição são escolhas de implementação. Pesquisa mantém o catálogo existente; não constitui pesquisa global de legislação/licenciamento nem nova restrição organizacional.",Cenário→cálculo→gravação→resultado/dossier correto→nova sessão→PDF/derivadas autorizadas. Sem aprovação/score inventado. Critério individual original preservado; cobertura atual ainda não individualizada.,"SimulationExecution, resultados/cenários/fontes, rascunhos e dossier existentes","Regras financeiras/fiscais separadas; fonte oficial aceite, parâmetros/âmbito/período podem faltar; não substituir por0/taxa atual.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: simuladores. Esforço de código adicional só após lacuna reproduzida.,"outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | 23/10/2026 — adequação do hero por pedido expresso da Tempo: docs/investmoz/public-portal-hero-validation.json; tools/portal-hero-qa.mjs; outputs/portal-hero-20261023/qa-results.json e capturas. Componentes reais com API/navegação sintéticas, sem base activa. Prova limitada ao hero, pesquisa de oportunidades e preservação da entrada documental; critérios mais amplos não encerrados.",simuladores,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Services/SimulationTaxParameterResolver.php; resources/js/components/simulation-form.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"SimulationExecutionTest, SimulationFormDraftTest, SimulationDraftContinuityTest, SimulationTaxRuleGovernanceTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,,,"0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, +MASTER-9dee08d0-0016,"Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 208 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 195 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 208 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 213,simuladores,/api/v1/questionnaires/{code}; /api/v1/simulation-drafts; /api/v1/simulation-drafts/claim; /api/v1/simulation-drafts/claim-preview; /api/v1/simulation-drafts/current; /api/v1/simulation-drafts/linkable-businesses; /api/v1/simulation-drafts/{id}; /api/v1/simulation-drafts/{id}/business-link; /api/v1/simulation-drafts/{id}/conversions; /api/v1/simulation-drafts/{id}/touch; /api/v1/simulations; /api/v1/simulations/history,"SimulationExecution, resultados/cenários/fontes, rascunhos e dossier existentes. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.","Ativo investemoz:5432, 295 tabelas, COUNTRY02/030000/040000 instalados; seis categorias e administração habilitadas. Leitura atual não demonstra esta cláusula individual. Legado, percurso integrado, modulares/ofertas e notificações desabilitados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Sem evidência suficiente,Cenário→cálculo→gravação→resultado/dossier correto→nova sessão→PDF/derivadas autorizadas. Sem aprovação/score inventado. Critério individual original preservado; cobertura atual ainda não individualizada.,"Regras financeiras/fiscais separadas; fonte oficial aceite, parâmetros/âmbito/período podem faltar; não substituir por0/taxa atual. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","MASTER-9dee08d0-0016: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania. | MASTER-efd550fd-0016: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania. | MASTER-9f5719c8-0017: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania. | MASTER-626cae3d-0014: Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania.",P2,MASTER-9dee08d0-0016: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0016: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0017: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0014: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0016 | MASTER-efd550fd-0016 | MASTER-9f5719c8-0017 | MASTER-626cae3d-0014,4,"[{""id"":""MASTER-9dee08d0-0016"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 208""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""},{""id"":""MASTER-efd550fd-0016"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 195""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""},{""id"":""MASTER-9f5719c8-0017"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 208""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""},{""id"":""MASTER-626cae3d-0014"",""pedido"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 213""],""aceitacao"":""Barra de Pesquisa Descontextualizada: O campo de busca deve sugerir temas directamente ligados ao investimento, como “registo de projecto na APIEX”, “benefícios fiscais”, “repatriamento de dividendos”, “Zonas Económicas Especiais” e “licenciamento simplificado”, em vez de exemplos associados a serviços gerais de cidadania."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""simuladores""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.simulation_form_drafts: 22 registos; public.simulation_scenarios: 16 registos; public.simulations: 16 registos; public.simulation_tax_rules: tabela ausente,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global. | 23/10/2026 — adequação do hero por pedido expresso da Tempo. Aplicadas as orientações permanentes AGENTS.md de 12/10 e 22/10/2026: regras da Tempo, escolhas técnicas identificadas e preservação de dados/histórico. Rotação, WebP e composição são escolhas de implementação. Pesquisa mantém o catálogo existente; não constitui pesquisa global de legislação/licenciamento nem nova restrição organizacional.",Cenário→cálculo→gravação→resultado/dossier correto→nova sessão→PDF/derivadas autorizadas. Sem aprovação/score inventado. Critério individual original preservado; cobertura atual ainda não individualizada.,"SimulationExecution, resultados/cenários/fontes, rascunhos e dossier existentes","Regras financeiras/fiscais separadas; fonte oficial aceite, parâmetros/âmbito/período podem faltar; não substituir por0/taxa atual.",Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,Reconciliação e prova individual antes de estimar alteração,Estimativa partilhada no lote correspondente de visao-consolidada-modulos.md; não somar por linha/cláusula/campo. Grupo: simuladores. Esforço de código adicional só após lacuna reproduzida.,"outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual. | 23/10/2026 — adequação do hero por pedido expresso da Tempo: docs/investmoz/public-portal-hero-validation.json; tools/portal-hero-qa.mjs; outputs/portal-hero-20261023/qa-results.json e capturas. Componentes reais com API/navegação sintéticas, sem base activa. Prova limitada ao hero, pesquisa de oportunidades e preservação da entrada documental; critérios mais amplos não encerrados.",simuladores,12/10/2026 — reconciliação e HTTP; nova inspeção visual bloqueada,,,Verificar individualmente este critério no contrato e percurso aplicável; a matriz inicial conserva texto/fonte sem o promover a aceitação.,"QA de UI quando browser disponível; reutilizar o domínio existente. Dados/decisões específicos no backlog, sem segunda validação de fonte oficial.",app/Services/SimulationTaxParameterResolver.php; resources/js/components/simulation-form.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"SimulationExecutionTest, SimulationFormDraftTest, SimulationDraftContinuityTest, SimulationTaxRuleGovernanceTest; suite global QA e GET por perfil disponíveis. Evidência do domínio não demonstra integralmente esta cláusula; UI individual pendente.",Não demonstrado individualmente; localizar critério e executar em QA.,,," | 03/11/2026: pesquisa rápida local em preparação; páginas e sete grupos públicos, palavras em ordem livre e sem acentos, paginação e aviso de fonte indisponível. Provas isoladas sem BD; consulta real, desempenho, ficheiros, dados privados e cobertura integral pendentes. Não instalada no activo. | 04/11/2026: candidata local com cores Tempo, resultados combinados por relevância e paginação global de cinco, variantes ortográficas e pesquisa de edições editoriais públicas; provas isoladas sem base. Consulta e desempenho com dados reais, ficheiros originais e módulos privados continuam pendentes; sem instalação no activo.","0,5–2 h de análise/prova por cláusula sem correspondência, agrupada por contrato/consumidor; não somar estas faixas como projetos independentes. Implementação indeterminada até localizar lacuna.",,, MASTER-9dee08d0-0017,"Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 209 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 196 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 209 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 214,oportunidades,/api/v1/business-opportunities; /api/v1/business-opportunities/{slug}; /investidor/terra-duat; /investidor/terra-duat/novo; /investidor/terra-duat/{id}; /investidor/terra-duat/{id}/actualizacoes; /investidor/terra-duat/{id}/documentos; /investidor/terra-duat/{id}/documentos/{attachment}; /investidor/terra-duat/{id}/editar; /investidor/terra-duat/{id}/imprimir; /investidor/terra-duat/{id}/solicitar-apoio; /management/opportunities,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual.; SectorInvestment e páginas existentes: conteúdo, oportunidades públicas, associações cadastradas e ações; contexto no mesmo InvestorPath e pré-seleção canónica do setor.","Correcção sectorial669cb897 instalada27/10,validada sobre9fda6d6e;cartões para URLs próprias sem apresentação de todos os detalhes. | Histórico preservado: Disponibilizado em 15/10/2026, setores-fintech-QA-d5b4dc354265 DOC01: 16 fontes e compilação; sete entradas setoriais e especialização Fintech no dossier; três artigos publicados (recursos reais 27/28/29), imagem 24 reutilizada por identidade/hash. Flags existentes preservadas; sem migrations/categorias novas. B e COUNTRY02 preservados.",outputs/investmoz-audit-current-20261012/runtime-active.json; runtime-qa18445.json. Inventário/GET do domínio não demonstra esta cláusula individual.,Implementado parcialmente — entradas setoriais e três artigos ativos,Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada.,"030000→040000 já instalados; contratos/resources existentes, sessão/MFA, ClamD e worker para ficheiros; gates das seis categorias ON. Variantes modulares/offers continuam separadas. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.","MASTER-9dee08d0-0017: Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo. | MASTER-efd550fd-0017: Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo. | MASTER-9f5719c8-0018: Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo. | MASTER-626cae3d-0015: Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo.",P2,MASTER-9dee08d0-0017: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0017: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0018: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0015: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0017 | MASTER-efd550fd-0017 | MASTER-9f5719c8-0018 | MASTER-626cae3d-0015,4,"[{""id"":""MASTER-9dee08d0-0017"",""pedido"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 209""],""aceitacao"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""},{""id"":""MASTER-efd550fd-0017"",""pedido"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 196""],""aceitacao"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""},{""id"":""MASTER-9f5719c8-0018"",""pedido"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 209""],""aceitacao"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""},{""id"":""MASTER-626cae3d-0015"",""pedido"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 214""],""aceitacao"":""Falta de Profundidade nos Cards: As descrições dos sectores estratégicos devem abandonar frases genéricas e apresentar informação específica de cada área, incluindo oportunidades, localização, tipo de investidor-alvo, principais incentivos, entidades competentes e documentos necessários para iniciar o processo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""}]",Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Não demonstrado individualmente; localizar critério e executar em QA.,Texto exatamente equivalente no mesmo módulo; sem fusão semântica aproximada,parcial,public.opportunities: 6 registos; public.opportunity_images: tabela ausente; public.opportunity_documents: tabela ausente; public.opportunity_location_drafts: tabela ausente,"A evidência existente é do domínio ou uma declaração histórica; não identifica a demonstração integral desta cláusula. Não afirmar implementado/parcial apenas por componente, menu ou teste global. | QA 15/10/2026: setores-fintech-QA-d5b4dc354265; sete entradas comerciais/contexto no dossier e três artigos preparados. B/COUNTRY02/aceitações preservados; sem disponibilização ativa. | Disponibilização expressamente autorizada pela Tempo: «Autorizo a disponibilizar». Executada com backup/restauro fresco verificado; apenas conferência operacional, sem repetir aceitação QA. [RECONCILIACAO-43-20261019] 5: Sem saldo de código no recorte visual aceite.","Reconciliar contrato/cláusula e provas por categoria; completar apenas falha/variante executável identificada. Response normativo não é completo. Critério individual original preservado; cobertura atual ainda não individualizada. | Entrega operacional deste delta concluída. Mercado Financeiro geral vazio e restante acervo mantêm os seus estados. Licenças, requisitos e documentos específicos Fintech por atividade permanecem por determinar; não bloqueiam a preparação independente nem se consideram concluídos. Mantém-se o detalhe integral de incentivos, entidades e documentos específicos por setor por completar. | Recorte QA demonstrado, sem encerrar critérios globais. Histórico económico combinado conservado, fora de novas escolhas; oportunidade6 não inferida como Fintech. Sem fusões por nome. Recuperação existente conferida por leitura e hashes; outra sessão/dispositivo e percursos aceites não reensaiados. Conferência dos anexos pelo utilizador pendente. | Recorte aprovado disponibilizado; requisitos globais permanecem abertos. Atividade historica combinada e oportunidade6 sem reclassificacao. Conferencia dos anexos finais pelo utilizador pendente. Sem repetir testes aceites, formularios ou L1. diff --git a/resources/js/components/portal-hero.tsx b/resources/js/components/portal-hero.tsx index 172888d5..50f5e566 100644 --- a/resources/js/components/portal-hero.tsx +++ b/resources/js/components/portal-hero.tsx @@ -231,14 +231,14 @@ export default function PortalHero({
setQuery(event.target.value)} - placeholder="Ex.: agronegócio, Nampula, energia..." + placeholder="Ex.: agronegócio, Nampula, legislação..." className="h-12 w-full min-w-0 flex-1 rounded-xl border-0 bg-white px-4 text-base text-slate-900 outline-none placeholder:text-slate-500" /> + +
+ +
+

Pesquise por palavras em qualquer ordem. Pode escrever com ou sem acentos. Os documentos são pesquisados pelos seus dados descritivos.

+ {unavailable.length > 0 &&

Não foi possível consultar: {unavailable.join(', ')}. Os restantes resultados continuam disponíveis; tente novamente mais tarde.

} + {query &&

{total > 0 ? `Resultados ${start}–${end} de ${total}` : 'Nenhum resultado nas fontes consultadas'} para “{query}”.

} + {query && total === 0 &&

Experimente outras palavras ou uma expressão mais curta.

} +
    + {data.map((item) =>
  1. +

    {item.category}

    + {item.title} + {item.description &&

    {item.description}

    } +
  2. )} +
+ {last > 1 && } +
+ ; +} diff --git a/routes/web.php b/routes/web.php index 98dc3f99..35c2f5b5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -98,6 +98,7 @@ Route::get('/artigos/{id}', [\App\Http\Controllers\EditorialArticleController::c Route::get('/carteira', [InvestmentPortfolioController::class, 'index'])->name('portfolio.index'); Route::get('/carteira/{id}', [InvestmentPortfolioController::class, 'show'])->whereUuid('id')->name('portfolio.show'); Route::get('/carteira/{id}/interesse', [InvestmentPortfolioController::class, 'resumeInterest'])->whereUuid('id')->middleware(['auth', \App\Http\Middleware\VerifiedOrSimpleParticipation::class])->name('portfolio.interest'); +Route::get('/pesquisa', \App\Http\Controllers\GlobalSearchController::class)->name('portal.search'); Route::get('/oportunidades', [InvestmentDiscoveryController::class, 'index'])->name('portal.opportunities'); Route::get('/oportunidades/{slug}', [InvestmentDiscoveryController::class, 'detail'])->name('portal.opportunity'); Route::get('/negocios', [PublicBusinessOpportunityController::class, 'index'])->name('business-marketplace.index'); diff --git a/tests/Feature/GlobalSearchTest.php b/tests/Feature/GlobalSearchTest.php new file mode 100644 index 00000000..10660758 --- /dev/null +++ b/tests/Feature/GlobalSearchTest.php @@ -0,0 +1,55 @@ +withoutVite(); + } + + public function test_empty_and_overlong_queries(): void + { + $this->get('/pesquisa')->assertOk()->assertInertia(fn (Assert $page) => $page->component('portal/search')->where('results.total', 0)); + $this->get('/pesquisa?q='.str_repeat('a', 121))->assertSessionHasErrors('q'); + } + + public function test_source_failure_recalculates_the_page_without_hiding_the_other_results(): void + { + $sources = [ + 'Documentos' => fn () => ['count' => 1, 'fetch' => fn () => throw new QueryException('testing', 'select 1', [], new \RuntimeException('Source unavailable'))], + 'Páginas' => fn () => ['count' => 1, 'fetch' => fn ($offset, $limit) => array_slice([['title' => 'Como investir', 'url' => '/como-investir']], $offset, $limit)], + ]; + $result = app(FederatedSearch::class)->page($sources, 1); + $this->assertSame(1, $result['total']); + $this->assertSame(['Documentos'], $result['unavailable']); + $this->assertSame('Como investir', $result['data'][0]['title']); + } + + public function test_public_search_uses_the_existing_portfolio_visibility_scope(): void + { + if (getenv('GLOBAL_SEARCH_QA') !== '1') { + $this->markTestSkipped('A prova com dados exige a base QA explicitamente seleccionada.'); + } + $identity = DB::selectOne('select current_database() as name, inet_server_port() as port'); + $this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name); + $this->assertSame(55441, (int) $identity->port); + $record = InvestmentPortfolioController::visible()->first(); + if (! $record) { + $this->markTestSkipped('No published portfolio records in isolated QA.'); + } + $term = mb_substr($record->title, 0, 15); + $this->get('/pesquisa?q='.urlencode($term))->assertOk()->assertInertia(fn (Assert $page) => $page + ->component('portal/search')->where('query', $term) + ->where('results.total', fn ($total) => $total >= 1)); + } +} diff --git a/tests/Unit/FederatedSearchTest.php b/tests/Unit/FederatedSearchTest.php new file mode 100644 index 00000000..244bd13e --- /dev/null +++ b/tests/Unit/FederatedSearchTest.php @@ -0,0 +1,47 @@ + $count, 'fetch' => function ($offset, $limit) use (&$calls, $prefix, $count) { + $calls[] = [$prefix, $offset, $limit]; + + return array_map(fn ($i) => ['title' => $prefix.$i, 'url' => '/'.$prefix.$i], array_slice(range(1, $count), $offset, $limit)); + }]; + }; + }; + $sources = ['Páginas' => $factory('p', 3), 'Oportunidades' => $factory('o', 7), 'Documentos' => $factory('d', 2)]; + $service = new FederatedSearch; + $first = $service->page($sources, 1); + $this->assertSame(['p1', 'p2', 'p3', 'o1', 'o2'], array_column($first['data'], 'title')); + $this->assertSame(['Páginas', 'Páginas', 'Páginas', 'Oportunidades', 'Oportunidades'], array_column($first['data'], 'category')); + $this->assertSame(12, $first['total']); + $this->assertSame(3, $first['last_page']); + $calls = []; + $second = $service->page($sources, 2); + $this->assertSame(['o3', 'o4', 'o5', 'o6', 'o7'], array_column($second['data'], 'title')); + $this->assertSame([['p', 0, 5], ['o', 0, 5], ['d', 0, 5], ['o', 5, 5]], $calls); + $third = $service->page($sources, 99); + $this->assertSame(3, $third['current_page']); + $this->assertSame(['d1', 'd2'], array_column($third['data'], 'title')); + } + + public function test_more_relevant_titles_appear_first_across_sources(): void + { + $factories = [ + 'Serviços' => fn () => ['count' => 1, 'fetch' => fn ($offset, $limit) => array_slice([['title' => 'Informação', 'url' => '/info', 'score' => 10]], $offset, $limit)], + 'Sectores' => fn () => ['count' => 1, 'fetch' => fn ($offset, $limit) => array_slice([['title' => 'Sectores', 'url' => '/sectores', 'score' => 100]], $offset, $limit)], + ]; + $result = (new FederatedSearch)->page($factories, 1, 5, fn ($item) => $item['score']); + $this->assertSame(['Sectores', 'Informação'], array_column($result['data'], 'title')); + } +} diff --git a/tests/Unit/SearchTermsTest.php b/tests/Unit/SearchTermsTest.php new file mode 100644 index 00000000..fe9a1f57 --- /dev/null +++ b/tests/Unit/SearchTermsTest.php @@ -0,0 +1,59 @@ +tokens(' NAMPULA agronegócio Agronegócio '); + + $this->assertSame(['nampula', 'agronegocio'], $tokens); + $this->assertTrue($search->matches('Agronegócio em Nampula', $tokens)); + $this->assertFalse($search->matches('Agronegócio em Gaza', $tokens)); + $this->assertSame([], $search->tokens(' ')); + } + + public function test_sql_matches_each_word_across_fields_with_bound_literal_patterns(): void + { + $connection = new PostgresConnection(fn () => throw new \LogicException('A database must not be opened.')); + $query = $connection->table('records'); + $search = new SearchTerms; + $search->constrain($query, ['title', 'description'], $search->tokens('100% a_b')); + + $sql = $query->toSql(); + $this->assertStringContainsString('translate(lower(coalesce(title::text', $sql); + $this->assertStringContainsString(' or ', $sql); + $this->assertStringContainsString(' and ', $sql); + $this->assertSame(['%100!%%', '%100!%%', '%a!_b%', '%a!_b%'], $query->getBindings()); + $this->assertStringNotContainsString('100%', $sql); + } + + public function test_static_services_can_be_found_without_a_database(): void + { + $pages = (new \ReflectionMethod(GlobalSearchController::class, 'pages'))->invoke(new GlobalSearchController); + $search = new SearchTerms; + $matches = array_values(array_filter($pages, fn ($page) => $search->matches(implode(' ', $page), $search->tokens('FISCAL simulador')))); + + $this->assertCount(1, $matches); + $this->assertSame('/simulador-fiscal', $matches[0]['url']); + } + + public function test_spelling_variants_common_words_and_title_relevance(): void + { + $search = new SearchTerms; + $tokens = $search->tokens('setores de investimento'); + $this->assertTrue($search->matches('Sectores de investimento', $tokens)); + $this->assertFalse($search->matches('Sector de saúde', $tokens)); + $this->assertTrue($search->matches('Projectos âncora', $search->tokens('projeto'))); + $this->assertTrue($search->matches('Oportunidades de investimento em energia', $search->tokens('Como investir em energia?'))); + $this->assertGreaterThan($search->score('Como investir', 'investir', ['investir']), $search->score('Investir', 'investir', ['investir'])); + $this->assertSame(['de'], $search->tokens('de')); + } +} diff --git a/tests/frontend/fiscal-form-entry.test.mjs b/tests/frontend/fiscal-form-entry.test.mjs index 6f87a202..b8905275 100644 --- a/tests/frontend/fiscal-form-entry.test.mjs +++ b/tests/frontend/fiscal-form-entry.test.mjs @@ -10,7 +10,7 @@ const require=createRequire(import.meta.url); const source=fs.readFileSync(new URL('../../resources/js/pages/portal/fiscal-forms.tsx',import.meta.url),'utf8'); const compiled=ts.transpileModule(source,{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2022,jsx:ts.JsxEmit.ReactJSX}}).outputText; const module={exports:{}}; -new Function('require','module','exports',compiled)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({href,children,...props})=>React.createElement('a',{href,...props},children),router:{on:()=>()=>{}}}:name==='@/layouts/portal-navigation'?{PortalMenu:()=>null}:name==='@/lib/fiscal-form-preparation'?forms:require(name),module,module.exports); +new Function('require','module','exports',compiled)(name=>name==='@inertiajs/react'?{Head:()=>null,Link:({href,children,...props})=>React.createElement('a',{href,...props},children),router:{on:()=>()=>{}}}:name==='@/components/portal-menu'?{PortalMenu:()=>null}:name==='@/lib/fiscal-form-preparation'?forms:require(name),module,module.exports); test('fiscal entry renders labelled editable fields and a guided preparation rather than legislation',()=>{ const html=renderToStaticMarkup(React.createElement(module.exports.default)); for(const label of ['Identificação e período','Nome ou designação social','NUIT','Período — ano e mês','Conferir e guardar','Guardar ficheiro para continuar','Recuperar ficheiro guardado','Confirmar recuperação']) { diff --git a/tests/frontend/global-search.test.mjs b/tests/frontend/global-search.test.mjs new file mode 100644 index 00000000..1820cd39 --- /dev/null +++ b/tests/frontend/global-search.test.mjs @@ -0,0 +1,36 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import ts from 'typescript'; + +const require = createRequire(import.meta.url); +const source = fs.readFileSync(new URL('../../resources/js/pages/portal/search.tsx', import.meta.url), 'utf8'); +const compiled = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText; +const module = { exports: {} }; +new Function('require', 'module', 'exports', compiled)(name => name === '@inertiajs/react' ? { + Head: () => null, + Link: ({ href, children, ...props }) => React.createElement('a', { href, ...props }, children), +} : name === 'lucide-react' ? { Search: () => null } : require(name), module, module.exports); +const Search = module.exports.default; + +test('shows five results in one sequence with numbered pagination and the query preserved', () => { + const results = { data: [{ title: 'Energia em Nampula', description: 'Projecto público', url: '/oportunidades/energia', category: 'Oportunidades' }], total: 12, current_page: 1, last_page: 3, per_page: 5, unavailable: [] }; + const html = renderToStaticMarkup(React.createElement(Search, { query: 'energia', results })); + assert.match(html, /Oportunidades/); + assert.match(html, /Resultados 1–1 de 12/); + assert.match(html, /href="\/oportunidades\/energia"/); + assert.match(html, /q=energia&page=2/); + assert.match(html, /aria-current="page"/); + assert.doesNotMatch(html, /emerald/); +}); + +test('makes source failures explicit without hiding other results', () => { + const results = { data: [], total: 0, current_page: 1, last_page: 1, per_page: 5, unavailable: ['Documentos e legislação'] }; + const html = renderToStaticMarkup(React.createElement(Search, { query: 'terra', results })); + assert.match(html, /role="alert"/); + assert.match(html, /Não foi possível consultar: Documentos e legislação/); + assert.match(html, /Nenhum resultado nas fontes consultadas/); +});