From f3455432cf77976312bd9066bf7fdcc9a2503884 Mon Sep 17 00:00:00 2001 From: Paulo Massingue Date: Tue, 3 Nov 2026 15:31:25 +0200 Subject: [PATCH 1/3] . --- .env.example | 5 + .../Api/TranslationApiController.php | 58 ++ config/services.php | 6 + .../v2/auditoria-portal/matriz-requisitos.csv | 4 +- .../roteiro-manual-qa-i0-i3.md | 12 + resources/js/components/input-error.tsx | 4 +- .../institutional-source/source-text.tsx | 55 +- resources/js/components/password-input.tsx | 4 +- resources/js/components/portal-menu.tsx | 9 + resources/js/components/tempo-brand.tsx | 48 +- .../js/components/tempo-header-tools.tsx | 33 +- .../js/layouts/app/app-sidebar-layout.tsx | 2 +- resources/js/layouts/auth-layout.tsx | 4 +- resources/js/layouts/portal-navigation.tsx | 39 +- resources/js/pages/PortalInvestidor.tsx | 2 +- resources/js/pages/auth/login.tsx | 24 +- resources/js/pages/auth/register.tsx | 22 +- resources/js/pages/investment/shared.tsx | 2 +- resources/js/pages/portal/article.tsx | 2 +- resources/js/pages/portal/document.tsx | 2 +- resources/js/pages/portal/services.tsx | 2 +- resources/js/translation/page-translator.tsx | 119 ++++ resources/js/translation/service.ts | 162 +++++ resources/js/translation/types.ts | 41 ++ resources/js/translation/utils.ts | 615 ++++++++++++++++++ routes/api.php | 4 + tests/Feature/TranslationApiTest.php | 27 + 27 files changed, 1240 insertions(+), 67 deletions(-) create mode 100644 app/Http/Controllers/Api/TranslationApiController.php create mode 100644 resources/js/components/portal-menu.tsx create mode 100644 resources/js/translation/page-translator.tsx create mode 100644 resources/js/translation/service.ts create mode 100644 resources/js/translation/types.ts create mode 100644 resources/js/translation/utils.ts create mode 100644 tests/Feature/TranslationApiTest.php diff --git a/.env.example b/.env.example index 0eb72f1a..f2fe62ce 100644 --- a/.env.example +++ b/.env.example @@ -119,3 +119,8 @@ INVESTMOZ_TAX_RULE_APPROVERS= INVESTMOZ_TAX_RULE_PUBLISHERS= INVESTMOZ_ZONE_TERRITORY_REVIEWERS= + +# Local LibreTranslate service and installed language models. +LIBRETRANSLATE_URL=http://127.0.0.1:5000 +LIBRETRANSLATE_TIMEOUT=60 +LIBRETRANSLATE_API_KEY= diff --git a/app/Http/Controllers/Api/TranslationApiController.php b/app/Http/Controllers/Api/TranslationApiController.php new file mode 100644 index 00000000..7f4c8a59 --- /dev/null +++ b/app/Http/Controllers/Api/TranslationApiController.php @@ -0,0 +1,58 @@ +timeout(config('services.translation.timeout'))->get($this->url().'/languages'); + if ($response->successful() && is_array($response->json())) { + return response()->json($response->json())->header('Cache-Control', 'no-store'); + } + } catch (Throwable) { + } + + return response()->json(['message' => 'Serviço de tradução temporariamente indisponível.'], 503); + } + + public function translate(Request $request): JsonResponse + { + $data = $request->validate([ + 'text' => 'required|string|max:10000', + 'source_lang' => 'required|string|max:20', + 'target_lang' => 'required|string|max:20', + 'is_html' => 'boolean', + ]); + if ($data['source_lang'] === $data['target_lang']) { + return response()->json(['original_text' => $data['text'], 'translated_text' => $data['text'], 'source_lang' => $data['source_lang'], 'target_lang' => $data['target_lang']])->header('Cache-Control', 'no-store'); + } + + $payload = ['q' => $data['text'], 'source' => $data['source_lang'], 'target' => $data['target_lang'], 'format' => ($data['is_html'] ?? false) ? 'html' : 'text']; + if (config('services.translation.api_key')) { + $payload['api_key'] = config('services.translation.api_key'); + } + try { + $response = Http::connectTimeout(2)->timeout(config('services.translation.timeout'))->post($this->url().'/translate', $payload); + $translated = $response->json('translatedText'); + if ($response->successful() && is_string($translated) && $translated !== '') { + return response()->json(['original_text' => $data['text'], 'translated_text' => $translated, 'source_lang' => $data['source_lang'], 'target_lang' => $data['target_lang']])->header('Cache-Control', 'no-store'); + } + } catch (Throwable) { + } + + return response()->json(['message' => 'Serviço de tradução temporariamente indisponível.'], 503)->header('Cache-Control', 'no-store'); + } + + private function url(): string + { + return rtrim((string) config('services.translation.url'), '/'); + } +} diff --git a/config/services.php b/config/services.php index febaaea2..892da0b1 100644 --- a/config/services.php +++ b/config/services.php @@ -28,6 +28,12 @@ return [ | */ + 'translation' => [ + 'url' => env('LIBRETRANSLATE_URL', 'http://127.0.0.1:5000'), + 'timeout' => (int) env('LIBRETRANSLATE_TIMEOUT', 60), + 'api_key' => env('LIBRETRANSLATE_API_KEY'), + ], + 'postmark' => [ 'key' => env('POSTMARK_API_KEY'), ], diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index 31687696..6460099b 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -389,7 +389,7 @@ MASTER-9dee08d0-0004,"O portal deve servir como um balcão ou agregador facilita MASTER-9dee08d0-0005,Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:,portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 118 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 122 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 135,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-0005: Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor: | MASTER-efd550fd-0005: Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor: | MASTER-9f5719c8-0006: Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:,P2,MASTER-9dee08d0-0005: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0005: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0006: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0005 | MASTER-efd550fd-0005 | MASTER-9f5719c8-0006,3,"[{""id"":""MASTER-9dee08d0-0005"",""pedido"":""Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:"",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 118""],""aceitacao"":""Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:"",""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-0005"",""pedido"":""Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 122""],""aceitacao"":""Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:"",""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-0006"",""pedido"":""Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 135""],""aceitacao"":""Princípios de experiência do utilizador: O portal deve ser desenhado a partir das principais tarefas do investidor:"",""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-0006,"Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 124 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 128 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 141 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 145,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-0006: Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado. | MASTER-efd550fd-0006: Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado. | MASTER-9f5719c8-0007: Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado. | MASTER-626cae3d-0004: Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado.",P2,MASTER-9dee08d0-0006: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0006: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0007: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0004: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0006 | MASTER-efd550fd-0006 | MASTER-9f5719c8-0007 | MASTER-626cae3d-0004,4,"[{""id"":""MASTER-9dee08d0-0006"",""pedido"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 124""],""aceitacao"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""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-0006"",""pedido"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 128""],""aceitacao"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""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-0007"",""pedido"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 141""],""aceitacao"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""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-0004"",""pedido"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 145""],""aceitacao"":""Cada página deve reduzir dúvidas, evitar jargão excessivo, apresentar chamadas de acção claras e oferecer um caminho visível para apoio especializado."",""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-0007,"Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 125 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 129 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 142 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 146,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-0007: Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional. | MASTER-efd550fd-0007: Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional. | MASTER-9f5719c8-0008: Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional. | MASTER-626cae3d-0005: Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional.",P2,MASTER-9dee08d0-0007: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0007: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0008: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0005: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0007 | MASTER-efd550fd-0007 | MASTER-9f5719c8-0008 | MASTER-626cae3d-0005,4,"[{""id"":""MASTER-9dee08d0-0007"",""pedido"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 125""],""aceitacao"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""Balcão/orientação""},{""id"":""MASTER-efd550fd-0007"",""pedido"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 129""],""aceitacao"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""Balcão/orientação""},{""id"":""MASTER-9f5719c8-0008"",""pedido"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 142""],""aceitacao"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""Balcão/orientação""},{""id"":""MASTER-626cae3d-0005"",""pedido"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 146""],""aceitacao"":""Calculadora ou simulador de incentivos: permitir ao investidor estimar, de forma orientadora, possíveis impostos, benefícios fiscais e requisitos aplicáveis em função da província, do sector, da localização em ZEE ou ZFI e do volume de investimento, deixando claro que o resultado depende de validação legal e institucional."",""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-0008,"Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 126,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. 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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-9dee08d0-0008: Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos.",P2,MASTER-9dee08d0-0008: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0008,1,"[{""id"":""MASTER-9dee08d0-0008"",""pedido"":""Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 126""],""aceitacao"":""Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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.,documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-0008,"Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 126,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. [02/11/2026: levantamento de codigo e incremento parcial preparados; quatro linguas solicitadas, cobertura transversal pendente. Ver roteiro-manual-qa-i0-i3.md.]","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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-9dee08d0-0008: Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos.",P2,MASTER-9dee08d0-0008: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0008,1,"[{""id"":""MASTER-9dee08d0-0008"",""pedido"":""Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 126""],""aceitacao"":""Suporte multilingue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de ação e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manutenção dos conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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.,documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-0009,"Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 127 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 131 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 144 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 148,CRM/solicitações,//; /Simulador; /api/v1/documents/recommendations; /api/v1/investment/submissions; /api/v1/investment/submissions/{id}; /api/v1/investment/submissions/{id}/attachments; /api/v1/investment/submissions/{id}/attachments/{attachment}; /api/v1/investment/submissions/{id}/submit; /apoio; /balcao-virtual; /como-investir; /documentos,"investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente. 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,"Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-9dee08d0-0009: Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento. | MASTER-efd550fd-0009: Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento. | MASTER-9f5719c8-0010: Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento. | MASTER-626cae3d-0007: Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento.",P2,MASTER-9dee08d0-0009: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0009: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0010: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0007: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0009 | MASTER-efd550fd-0009 | MASTER-9f5719c8-0010 | MASTER-626cae3d-0007,4,"[{""id"":""MASTER-9dee08d0-0009"",""pedido"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 127""],""aceitacao"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-efd550fd-0009"",""pedido"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 131""],""aceitacao"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-9f5719c8-0010"",""pedido"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 144""],""aceitacao"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""},{""id"":""MASTER-626cae3d-0007"",""pedido"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 148""],""aceitacao"":""Área Reservada ou Dashboard do Investidor: permitir ao investidor acompanhar o estado das submissões, visualizar documentos pendentes, receber notificações, agendar reuniões virtuais, consultar histórico de interacções e solicitar assistência direta junto dos serviços de facilitação do investimento."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""CRM/solicitações""}]",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.interest_requests: 4 registos; public.leads: 0 registos; public.investors: 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.","Converter sem duplicar, tarefa/responsável/próxima ação recuperáveis; corrigir apenas falha comprovada. Critério individual original preservado; cobertura atual ainda não individualizada.","investors/contacts/leads/tasks, OpportunityContacts, acompanhamento interno existente",Relações mestre/contexto e autorizações; não depende de CRM externo. C131 sóQA.,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: acompanhamento. 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.,acompanhamento,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/CrmController.php; app/Services/DeclaredOriginStatistics.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"InterestSubmissionTest, InvestmentWorkspaceTest, PortalOriginCompatibilityTest; 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-0010,"Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio constante.",portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 129,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. 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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-9dee08d0-0010: Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio constante.",P2,MASTER-9dee08d0-0010: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0010,1,"[{""id"":""MASTER-9dee08d0-0010"",""pedido"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio constante."",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 129""],""aceitacao"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio constante."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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. | 21/10/2026: pedido expresso da Tempo para uma única navbar, sem retirar acessos; aplicação de AGENTS.md, decisões de 12/10/2026. Agrupamento Tempo, posições, breakpoint 1280px e altura 72px são escolhas de apresentação, não novas regras operacionais. Prova local isolada do header em tools/portal-header-qa.mjs e outputs/header-navbar-20261021/qa-results.json; não encerra o critério integral nem representa implantação no activo.",documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-0011,A navegação deve reflectir o ciclo de vida do investidor:,portal do investidor\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 134 | portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 135 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 148 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 152,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-0011: A navegação deve reflectir o ciclo de vida do investidor: | MASTER-efd550fd-0011: A navegação deve reflectir o ciclo de vida do investidor: | MASTER-9f5719c8-0012: A navegação deve reflectir o ciclo de vida do investidor: | MASTER-626cae3d-0009: A navegação deve reflectir o ciclo de vida do investidor:,P2,MASTER-9dee08d0-0011: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-efd550fd-0011: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0012: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0009: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-9dee08d0-0011 | MASTER-efd550fd-0011 | MASTER-9f5719c8-0012 | MASTER-626cae3d-0009,4,"[{""id"":""MASTER-9dee08d0-0011"",""pedido"":""A navegação deve reflectir o ciclo de vida do investidor:"",""fontes"":[""portal do investidor\\Reformulacao do portal do investidor.docx; SHA 9dee08d0ebbbb262651a01ffc7e0ade97323664bda59fe8ce0b07ead82736ce1; w:p 134""],""aceitacao"":""A navegação deve reflectir o ciclo de vida do investidor:"",""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-0011"",""pedido"":""A navegação deve reflectir o ciclo de vida do investidor:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 135""],""aceitacao"":""A navegação deve reflectir o ciclo de vida do investidor:"",""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-0012"",""pedido"":""A navegação deve reflectir o ciclo de vida do investidor:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 148""],""aceitacao"":""A navegação deve reflectir o ciclo de vida do investidor:"",""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-0009"",""pedido"":""A navegação deve reflectir o ciclo de vida do investidor:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 152""],""aceitacao"":""A navegação deve reflectir o ciclo de vida do investidor:"",""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. @@ -950,7 +950,7 @@ MASTER-f70ce307-0002,5.9.1. Documentos obrigatórios e controlos dos campos MIN2 MASTER-51b8f0ad-0002,5.9.1. Documentos obrigatórios e controlos dos campos MIN62,portal do investidor\Portal V4\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 72,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. 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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,MASTER-51b8f0ad-0002: 5.9.1. Documentos obrigatórios e controlos dos campos MIN62,P2,MASTER-51b8f0ad-0002: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-51b8f0ad-0002,1,"[{""id"":""MASTER-51b8f0ad-0002"",""pedido"":""5.9.1. Documentos obrigatórios e controlos dos campos MIN62"",""fontes"":[""portal do investidor\\Portal V4\\upgrade do portal de investimentos v2.docx; SHA 51b8f0ad3296d1064a5c12b450874fc72640869f10a164319cd7f3153dd3a086; w:p 72""],""aceitacao"":""5.9.1. Documentos obrigatórios e controlos dos campos MIN62"",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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.,documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-efd550fd-0002,"Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade.",portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 98 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 111,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.","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,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-efd550fd-0002: Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade. | MASTER-9f5719c8-0003: Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade.",P2,MASTER-efd550fd-0002: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0003: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0002 | MASTER-9f5719c8-0003,2,"[{""id"":""MASTER-efd550fd-0002"",""pedido"":""Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 98""],""aceitacao"":""Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""},{""id"":""MASTER-9f5719c8-0003"",""pedido"":""Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 111""],""aceitacao"":""Facilitação e acompanhamento: permitir que os investidores identifiquem os requisitos, as entidades competentes, os documentos necessários, os incentivos aplicáveis e os canais de contacto para cada oportunidade."",""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.",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.,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources","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.,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: oportunidades. 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.,oportunidades,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/OpportunityManagement.php; app/Services/OpportunityResources.php; resources/js/pages/management/opportunity-draft.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"OpportunityManagementTest, OpportunityResourcesTest, OpportunityOriginReviewTest; 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-efd550fd-0003,Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:,portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 102 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 115 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 124,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-efd550fd-0003: Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem: | MASTER-9f5719c8-0004: Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem: | MASTER-626cae3d-0001: Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:,P2,MASTER-efd550fd-0003: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0004: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0001: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0003 | MASTER-9f5719c8-0004 | MASTER-626cae3d-0001,3,"[{""id"":""MASTER-efd550fd-0003"",""pedido"":""Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 102""],""aceitacao"":""Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:"",""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-0004"",""pedido"":""Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 115""],""aceitacao"":""Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:"",""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-0001"",""pedido"":""Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:"",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 124""],""aceitacao"":""Os eixos estruturantes e os requisitos funcionais que devem estar presentes incluem:"",""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-efd550fd-0008,"Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos.",portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 130 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 143 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 147,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. 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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-efd550fd-0008: Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos. | MASTER-9f5719c8-0009: Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos. | MASTER-626cae3d-0006: Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos.",P2,MASTER-efd550fd-0008: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0009: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0006: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0008 | MASTER-9f5719c8-0009 | MASTER-626cae3d-0006,3,"[{""id"":""MASTER-efd550fd-0008"",""pedido"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 130""],""aceitacao"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""},{""id"":""MASTER-9f5719c8-0009"",""pedido"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 143""],""aceitacao"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""},{""id"":""MASTER-626cae3d-0006"",""pedido"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 147""],""aceitacao"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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.,documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-efd550fd-0008,"Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos.",portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 130 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 143 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 147,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. Capacidade existente associada ao domínio; correspondência integral desta cláusula só quando demonstrada na prova individual. [02/11/2026: levantamento de codigo e incremento parcial preparados; quatro linguas solicitadas, cobertura transversal pendente. Ver roteiro-manual-qa-i0-i3.md.]","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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-efd550fd-0008: Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos. | MASTER-9f5719c8-0009: Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos. | MASTER-626cae3d-0006: Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos.",P2,MASTER-efd550fd-0008: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0009: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0006: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0008 | MASTER-9f5719c8-0009 | MASTER-626cae3d-0006,3,"[{""id"":""MASTER-efd550fd-0008"",""pedido"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 130""],""aceitacao"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""},{""id"":""MASTER-9f5719c8-0009"",""pedido"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 143""],""aceitacao"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""},{""id"":""MASTER-626cae3d-0006"",""pedido"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 147""],""aceitacao"":""Suporte multilíngue: disponibilizar, no mínimo, versões completas em português e inglês, com alternância visível em todas as páginas principais e tradução consistente dos formulários, guias, mensagens de erro, botões de acção e documentos de apoio essenciais. A inclusão futura de francês ou mandarim deve depender dos públicos-alvo prioritários e da capacidade de manter os conteúdos."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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.,documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-efd550fd-0010,"Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo.",portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 132 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 145 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 149,biblioteca,//; /Simulador; /api/v1/ai/documents; /api/v1/documents/recommendations; /api/v2/investment/admin/legal-sources; /api/v2/investment/admin/legal-sources/{id}/{action}; /apoio; /balcao-virtual; /balcao-virtual/dossiers/{id}/documentos/{document}/conferencia; /como-investir; /documentos; /documentos/{id},"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas. 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,Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF. Nenhuma nova decisão identificada para reconciliar/demonstrar esta cláusula. Decisões específicas só após localizar a regra correspondente.,"MASTER-efd550fd-0010: Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo. | MASTER-9f5719c8-0011: Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo. | MASTER-626cae3d-0008: Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo.",P2,MASTER-efd550fd-0010: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0011: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0008: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0010 | MASTER-9f5719c8-0011 | MASTER-626cae3d-0008,3,"[{""id"":""MASTER-efd550fd-0010"",""pedido"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 132""],""aceitacao"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""},{""id"":""MASTER-9f5719c8-0011"",""pedido"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 145""],""aceitacao"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""},{""id"":""MASTER-626cae3d-0008"",""pedido"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 149""],""aceitacao"":""Requisitos mínimos de acessibilidade: O portal deve garantir contraste adequado, leitura simples em dispositivos móveis, navegação por teclado, textos alternativos para imagens e ícones, rótulos claros em formulários e mensagens de erro compreensíveis. Estes requisitos são essenciais para que investidores com diferentes níveis de literacia digital consigam utilizar a plataforma sem apoio contínuo."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""biblioteca""}]",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.documents: 4 registos; public.portal_resources: 4 registos; public.portal_resource_links: 19 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.",Cobrir consulta/importação/doc/derivada/hash/perfil e revogação; preservar original; divulgação separada. Não inferir relação territorial. Critério individual original preservado; cobertura atual ainda não individualizada.,"documents, biblioteca, importação autenticada, VerifiedDocumentBinary, jobs/derivadas",Scanner/worker/GD/WebP e originais privados; ACL por oportunidade/sala diferente de partilha individual não implementada. LegadoOFF.,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: documentos e legado. 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.,documentos e legado,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/PortalServicesController.php; app/Jobs/ScanDocumentForMalware.php — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"PortalServicesTest, DocumentLibrarySchemaCompatibilityTest, ImportDocumentBatchTest, ClamDScannerIntegrationTest; 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-efd550fd-0012,"d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis.",portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 149 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 162 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 166,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.","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,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-efd550fd-0012: d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis. | MASTER-9f5719c8-0013: d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis. | MASTER-626cae3d-0010: d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis.",P2,MASTER-efd550fd-0012: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0013: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0010: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0012 | MASTER-9f5719c8-0013 | MASTER-626cae3d-0010,3,"[{""id"":""MASTER-efd550fd-0012"",""pedido"":""d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 149""],""aceitacao"":""d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""},{""id"":""MASTER-9f5719c8-0013"",""pedido"":""d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 162""],""aceitacao"":""d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis."",""estado"":""parcial"",""variantes"":""Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas"",""modulo"":""oportunidades""},{""id"":""MASTER-626cae3d-0010"",""pedido"":""d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 166""],""aceitacao"":""d) Carteira PII 2026-2030: cada sector deve apresentar oportunidades associadas ao PII 2026-2030, com indicação da localização, da entidade promotora, da fase de preparação, das necessidades de financiamento, do impacto económico esperado e dos documentos de referência disponíveis."",""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.",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.,"OpportunityManagement, OpportunityCanonicalContract/Response, OpportunityReviewMateriality, PortalProfileAccess, OpportunityResources","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.,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: oportunidades. 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.,oportunidades,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/OpportunityManagement.php; app/Services/OpportunityResources.php; resources/js/pages/management/opportunity-draft.tsx — componentes do domínio encontrados; correspondência integral deste requisito não presumida,"OpportunityManagementTest, OpportunityResourcesTest, OpportunityOriginReviewTest; 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-efd550fd-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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados.",portal do investidor\backup\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 157 | portal do investidor\backup\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 170 | portal do investidor\backup\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 174,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-efd550fd-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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados. | MASTER-9f5719c8-0014: 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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados. | MASTER-626cae3d-0011: 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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados.",P2,MASTER-efd550fd-0013: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-9f5719c8-0014: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas | MASTER-626cae3d-0011: Todas as ocorrências exatas preservadas nas fontes; variantes de texto têm linhas distintas,MASTER-efd550fd-0013 | MASTER-9f5719c8-0014 | MASTER-626cae3d-0011,3,"[{""id"":""MASTER-efd550fd-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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor 1.docx; SHA efd550fd2a9d2a21785d7b3c140d4608f1ba27c52f156e2709e9fbf388a1a569; w:p 157""],""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, da disponibilidade técnica das entidades públicas e da 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""},{""id"":""MASTER-9f5719c8-0014"",""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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v2.docx; SHA 9f5719c8b0cf78855f727d980b77a8f9409fc7f93e303e25d355d009a07d5734; w:p 170""],""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, da disponibilidade técnica das entidades públicas e da 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""},{""id"":""MASTER-626cae3d-0011"",""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, da disponibilidade técnica das entidades públicas e da validação jurídica dos fluxos de dados."",""fontes"":[""portal do investidor\\backup\\Reformulacao do portal do investidor v3.docx; SHA 626cae3d584658842ca36b658e7b5b3cc98d87036747e65b29274f0209734e5a; w:p 174""],""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, da disponibilidade técnica das entidades públicas e da 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.",,, diff --git a/docs/investmoz/v2/auditoria-portal/roteiro-manual-qa-i0-i3.md b/docs/investmoz/v2/auditoria-portal/roteiro-manual-qa-i0-i3.md index e624cdbc..da455a08 100644 --- a/docs/investmoz/v2/auditoria-portal/roteiro-manual-qa-i0-i3.md +++ b/docs/investmoz/v2/auditoria-portal/roteiro-manual-qa-i0-i3.md @@ -1,5 +1,17 @@ # Roteiro executável de interface — I0–I3 QA +## Tradução transversal — levantamento e plano, 02/11/2026 + +O pedido actual determina a tradução nas quatro línguas já apresentadas no selector: português, inglês, francês e espanhol. A referência anterior MASTER-9dee08d0-0008 prevê pelo menos português e inglês; o pedido actual explicita as quatro línguas já expostas. O levantamento é de código, não comprova a situação no ambiente activo nem substitui os recibos de reposição ou a comparação com o último estado demonstrado. + +- Existe um contexto de idioma persistido no navegador e um dicionário em `tempo-brand.tsx`; a sua cobertura anterior concentrava-se no menu e rodapé. A tradução devolve o original quando não encontra uma chave. Os componentes institucionais `Trans` recorrem ao mesmo dicionário, mas várias das suas frases não têm entrada. +- Há 125 ficheiros em `resources/js/pages` (incluindo ficheiros auxiliares): páginas públicas, autenticação, gestão, oportunidades, carteira, percurso, dossier, simulador, terra, parcerias e outras áreas contêm textos portugueses directos. Formulários e componentes partilhados em `resources/js/lib` e `resources/js/components` também contêm rótulos e mensagens directos. A IA possui selector e textos próprios em português/inglês, sem integração completa com o idioma global. Catálogos e questionários de API têm pontos fixados em `pt-MZ`; mensagens de validação Laravel estão em português. Documentos, dados cadastrados, fontes e exportações precisam de tratamento distinto da interface e preservação do original. +- Incremento de código preparado: idioma do documento HTML acompanha o selector; textos comuns do acesso ao conteúdo, caminho de navegação, aviso de homologação, contacto de apoio e selector passam pelo contexto; algumas etiquetas institucionais passam a ter entradas nas três línguas. Os títulos sem entrada continuam no original. Esta cobertura é parcial, não representa conclusão do requisito nem publicação no activo. +- Continuação: etiquetas e placeholders de acesso e registo, apresentação do respectivo título, controlo acessível de visualização da palavra-passe e duas mensagens exactas de autenticação usam o mesmo contexto. Mensagens variáveis, restantes validações, outros ecrãs de autenticação e conteúdos de QA continuam por traduzir. Valores submetidos e comportamento de autenticação não foram alterados. O `types:check` e o build do primeiro incremento passaram; este acréscimo requer nova verificação. +- Comparação com `D:\Projects\revistatempo` e `D:\Projects\Tempo_Investidor\tempo-api`: a Revista usa componentes/hooks explicitamente marcados, POST `/api/translate`, protecção de tokens e cache; a API chama LibreTranslate e devolve o original em caso de falha, inclusive com HTTP 200. Não se demonstrou cobertura integral automática na Revista. Adaptado o cliente para os textos institucionais públicos já marcados com `Trans`, com URL configurável por `VITE_TRANSLATION_API_URL`, preservação de tokens, deduplicação e cache em memória apenas para respostas diferentes do original. Este incremento não envia dados de formulários, dossiers ou outras informações privadas para o serviço. A conectividade real, cobertura restante, resultados e exportações continuam pendentes de verificação e implementação; não declarar a plataforma traduzida. + +Sequência restante: (1) inventariar por rota e componente, comparando com o último estado demonstrado e reutilizando provas e recibos existentes; (2) consolidar catálogo versionado e detecção de chaves ausentes para as quatro línguas; (3) migrar navegação e páginas públicas, autenticação e formulários; (4) migrar oportunidades, carteira, percurso adaptativo, dossier, simulador e áreas privadas/gestão sem alterar regras nem dados; (5) tratar mensagens do servidor, catálogos, conteúdos cadastrados e relatórios, preservando origens e indicando quando não existe versão traduzida; (6) verificar alternância, persistência, regressão dos fluxos, acessibilidade e exportações em QA isolado e registar cobertura por requisito nesta matriz e neste manual. Traduções de documentos oficiais não substituem os originais. + ## C37 — causa confirmada; seleção de conta QA corrigida — P0, visual pendente O pedido de reabertura referia a conta esperada18. A captura manual21:22–21:24UTC demonstra que foi selecionada a conta13 restaurada. O MFA foi alcançado, mas a autenticação nunca se completou. Causa concreta: credenciais MFA da conta13 cifradas pela chave de origem, incompatíveis com a chave estávelQA; a conta18 sintética não foi desafiada nessa tentativa. Nenhuma chave, MFA ou conta13 foi alterada para contornar o problema. diff --git a/resources/js/components/input-error.tsx b/resources/js/components/input-error.tsx index f323cee9..e3ba62b7 100644 --- a/resources/js/components/input-error.tsx +++ b/resources/js/components/input-error.tsx @@ -1,17 +1,19 @@ import type { HTMLAttributes } from 'react'; import { cn } from '@/lib/utils'; +import { useTempoUi } from '@/components/tempo-brand'; export default function InputError({ message, className = '', ...props }: HTMLAttributes & { message?: string }) { + const { t } = useTempoUi(); return message ? (

- {message} + {t(message)}

) : null; } diff --git a/resources/js/components/institutional-source/source-text.tsx b/resources/js/components/institutional-source/source-text.tsx index 4d375db5..ccd33aee 100644 --- a/resources/js/components/institutional-source/source-text.tsx +++ b/resources/js/components/institutional-source/source-text.tsx @@ -1,7 +1,54 @@ +import { useEffect, useState } from 'react'; import { useTempoUi } from '@/components/tempo-brand'; +import { translateText } from '@/translation/service'; +import { protectTranslationTokens, restoreTranslationTokens } from '@/translation/utils'; -// Bridge the original Trans calls to this platform's existing language context. -export function Trans({ children }: { children: string }) { - const { t } = useTempoUi(); - return {t(children)}; +// Only the fixed public institutional texts using this component are sent to Tempo. +const cache = new Map(); +const pending = new Map>(); + +function translateInstitutionalText(text: string, language: string): Promise { + const key = JSON.stringify([language, text]); + const cached = cache.get(key); + if (cached) return Promise.resolve(cached); + const existing = pending.get(key); + if (existing) return existing; + + const protectedText = protectTranslationTokens(text); + const request = translateText({ + text: protectedText.text, + sourceLanguage: 'pt', + targetLanguage: language, + }).then(({ translatedText }) => { + // Do not accept a response that lost an identifier, URL or protected term. + for (const token of protectedText.replacements.keys()) { + if (!translatedText.includes(token)) throw new Error('Translation lost a protected token'); + } + const result = restoreTranslationTokens(translatedText, protectedText.replacements); + // Tempo's API returns the input on upstream failure: do not cache it as success. + if (result !== text) cache.set(key, result); + return result; + }).finally(() => pending.delete(key)); + pending.set(key, request); + return request; +} + +export function Trans({ children }: { children: string }) { + const { language, t } = useTempoUi(); + const localText = t(children); + const [result, setResult] = useState<{ key: string; text: string } | null>(null); + const key = JSON.stringify([language, children]); + + useEffect(() => { + if (language === 'pt' || localText !== children || !children.trim()) return; + let active = true; + translateInstitutionalText(children, language).then((text) => { + if (active) setResult({ key, text }); + }).catch(() => { + if (active) setResult(null); + }); + return () => { active = false; }; + }, [children, key, language, localText]); + + return {result?.key === key ? result.text : localText}; } diff --git a/resources/js/components/password-input.tsx b/resources/js/components/password-input.tsx index 00f59d2d..020db8f2 100644 --- a/resources/js/components/password-input.tsx +++ b/resources/js/components/password-input.tsx @@ -3,6 +3,7 @@ import type { ComponentProps, Ref } from 'react'; import { useState } from 'react'; import { Input } from '@/components/ui/input'; import { cn } from '@/lib/utils'; +import { useTempoUi } from '@/components/tempo-brand'; export default function PasswordInput({ className, @@ -10,6 +11,7 @@ export default function PasswordInput({ ...props }: Omit, 'type'> & { ref?: Ref }) { const [showPassword, setShowPassword] = useState(false); + const { t } = useTempoUi(); return (
@@ -23,7 +25,7 @@ export default function PasswordInput({ type="button" onClick={() => setShowPassword((prev) => !prev)} className="text-muted-foreground hover:text-foreground focus-visible:ring-ring absolute inset-y-0 right-0 flex items-center rounded-r-md px-3 focus-visible:ring-[3px] focus-visible:outline-none" - aria-label={showPassword ? 'Ocultar palavra-passe' : 'Mostrar palavra-passe'} + aria-label={t(showPassword ? 'Ocultar palavra-passe' : 'Mostrar palavra-passe')} tabIndex={-1} > {showPassword ? ( diff --git a/resources/js/components/portal-menu.tsx b/resources/js/components/portal-menu.tsx new file mode 100644 index 00000000..0e88224d --- /dev/null +++ b/resources/js/components/portal-menu.tsx @@ -0,0 +1,9 @@ +import { createContext, useContext } from 'react'; +import { PortalGroupedMenu } from '@/components/portal-grouped-menu'; + +export const MenuPlacement = createContext(false); + +export function PortalMenu({ primary = false }: { primary?: boolean }) { + const placed = useContext(MenuPlacement); + return placed && !primary ? null : ; +} diff --git a/resources/js/components/tempo-brand.tsx b/resources/js/components/tempo-brand.tsx index 442bebf2..7675e556 100644 --- a/resources/js/components/tempo-brand.tsx +++ b/resources/js/components/tempo-brand.tsx @@ -241,6 +241,48 @@ const translations: Record> = { }, }; +const sharedTranslations: Record = { + 'Criar conta': ['Create account', 'Créer un compte', 'Crear cuenta'], + 'Entrar na sua conta': ['Sign in to your account', 'Connectez-vous à votre compte', 'Inicie sesión en su cuenta'], + 'Crie uma conta simples para continuar e acompanhar a sua operação. Não é necessário configurar MFA para participar.': ['Create an account to continue and follow your operation. You do not need to set up MFA to participate.', 'Créez un compte pour poursuivre et suivre votre opération. La configuration de l’authentification multifacteur n’est pas nécessaire pour participer.', 'Cree una cuenta para continuar y seguir su operación. No necesita configurar la autenticación multifactor para participar.'], + 'Entre para continuar a operação que escolheu. Os dados preenchidos permanecem guardados neste navegador.': ['Sign in to continue the operation you chose. Your entered data remains saved in this browser.', 'Connectez-vous pour poursuivre l’opération choisie. Les données saisies restent enregistrées dans ce navigateur.', 'Inicie sesión para continuar la operación elegida. Los datos introducidos permanecen guardados en este navegador.'], + 'Palavra-passe': ['Password', 'Mot de passe', 'Contraseña'], + 'Mostrar palavra-passe': ['Show password', 'Afficher le mot de passe', 'Mostrar contraseña'], + 'Ocultar palavra-passe': ['Hide password', 'Masquer le mot de passe', 'Ocultar contraseña'], + 'Confirmar palavra-passe': ['Confirm password', 'Confirmer le mot de passe', 'Confirmar contraseña'], + 'Nome': ['Name', 'Nom', 'Nombre'], + 'Nome completo': ['Full name', 'Nom complet', 'Nombre completo'], + 'Esqueceu a palavra-passe?': ['Forgot your password?', 'Mot de passe oublié ?', '¿Olvidó su contraseña?'], + 'Manter sessão iniciada': ['Keep me signed in', 'Rester connecté', 'Mantener la sesión iniciada'], + 'Ainda não tem conta?': ['Don’t have an account?', 'Vous n’avez pas de compte ?', '¿Aún no tiene cuenta?'], + 'Já tem conta?': ['Already have an account?', 'Vous avez déjà un compte ?', '¿Ya tiene cuenta?'], + 'Entrar com chave de acesso': ['Sign in with a passkey', 'Se connecter avec une clé d’accès', 'Iniciar sesión con clave de acceso'], + 'A autenticar…': ['Signing in…', 'Connexion en cours…', 'Iniciando sesión…'], + 'Ou continuar com email': ['Or continue with email', 'Ou continuer avec l’e-mail', 'O continuar con correo electrónico'], + 'O email ou a palavra-passe não correspondem aos nossos registos.': ['The email or password does not match our records.', 'L’adresse e-mail ou le mot de passe ne correspond pas à nos informations.', 'El correo electrónico o la contraseña no coinciden con nuestros registros.'], + 'A palavra-passe está incorrecta.': ['The password is incorrect.', 'Le mot de passe est incorrect.', 'La contraseña es incorrecta.'], + 'Atalho para o conteúdo': ['Skip link', 'Lien d’accès rapide', 'Enlace de acceso rápido'], + 'Saltar para o conteúdo': ['Skip to content', 'Aller au contenu', 'Ir al contenido'], + 'Caminho de navegação': ['Breadcrumb', 'Fil d’Ariane', 'Ruta de navegación'], + 'Contacto de apoio ao investidor': ['Investor support contact', 'Contact de l’assistance aux investisseurs', 'Contacto de apoyo al inversor'], + 'Contactar Apoio ao Investidor': ['Contact investor support', 'Contacter l’assistance aux investisseurs', 'Contactar con apoyo al inversor'], + 'Ambiente de homologação': ['Review environment', 'Environnement de validation', 'Entorno de validación'], + 'Homologação reservada à revisão da Tempo. Consulte o conteúdo público; entre na sua conta quando iniciar uma operação.': ['This review environment is reserved for Tempo. Browse public content; sign in when starting an operation.', 'Cet environnement de validation est réservé à Tempo. Consultez le contenu public ; connectez-vous pour commencer une opération.', 'Este entorno de validación está reservado para Tempo. Consulte el contenido público; inicie sesión al comenzar una operación.'], + 'Idioma da navegação': ['Navigation language', 'Langue de navigation', 'Idioma de navegación'], + 'Navegação institucional': ['Language', 'Langue', 'Idioma'], + 'Valores': ['Values', 'Valeurs', 'Valores'], + 'Ver menos': ['Show less', 'Voir moins', 'Ver menos'], + 'Ver mais': ['Show more', 'Voir plus', 'Ver más'], + 'Voltar': ['Back', 'Retour', 'Volver'], + 'Anos de Experiência': ['Years of experience', 'Années d’expérience', 'Años de experiencia'], + 'Projetos Realizados': ['Projects completed', 'Projets réalisés', 'Proyectos realizados'], +}; +for (const [source, values] of Object.entries(sharedTranslations)) { + (['en', 'fr', 'es'] as const).forEach((locale, index) => { + translations[locale][source] = values[index]; + }); +} + const TempoUi = createContext({ language: 'pt', setLanguage: (_language: string) => {}, @@ -255,13 +297,17 @@ export function TempoUiProvider({ children }: { children: ReactNode }) { useEffect(() => { try { const saved = localStorage.getItem('tempo-navigation-language'); - if (saved && ['pt', 'en', 'fr', 'es'].includes(saved)) + if (saved && /^[a-zA-Z][a-zA-Z0-9-]{0,19}$/.test(saved)) setCurrentLanguage(saved); } catch { /* Navigation remains usable without browser storage. */ } }, []); + useEffect(() => { + document.documentElement.lang = language; + }, [language]); const setLanguage = (next: string) => { + if (!/^[a-zA-Z][a-zA-Z0-9-]{0,19}$/.test(next)) return; setCurrentLanguage(next); try { localStorage.setItem('tempo-navigation-language', next); diff --git a/resources/js/components/tempo-header-tools.tsx b/resources/js/components/tempo-header-tools.tsx index 10ea7063..2296e4b9 100644 --- a/resources/js/components/tempo-header-tools.tsx +++ b/resources/js/components/tempo-header-tools.tsx @@ -8,6 +8,9 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { useTempoUi } from '@/components/tempo-brand'; +import { getLanguages } from '@/translation/service'; +import { withPortugueseFirst } from '@/translation/utils'; +import type { TranslationLanguage } from '@/translation/types'; type Weather = { temp: number; description: string; humidity?: number }; @@ -97,12 +100,28 @@ export function TempoWeather() { } export function TempoLanguageSelector() { - const { language, setLanguage } = useTempoUi(); + const { language, setLanguage, t } = useTempoUi(); + const [languages, setLanguages] = useState([ + { code: 'pt', name: 'Português' }, + { code: 'en', name: 'English' }, + { code: 'fr', name: 'Français' }, + { code: 'es', name: 'Español' }, + ]); + const [available, setAvailable] = useState(true); + useEffect(() => { + const controller = new AbortController(); + getLanguages(controller.signal).then((items) => { + const portuguese = items.find((item) => item.code === 'pt'); + if (!portuguese) throw new Error('Portuguese model unavailable'); + setLanguages(withPortugueseFirst(items.filter((item) => item.code === 'pt' || !portuguese.targets || portuguese.targets.includes(item.code)))); + }).catch(() => { if (!controller.signal.aborted) setAvailable(false); }); + return () => controller.abort(); + }, []); return (
@@ -127,15 +129,15 @@ export default function Login({ status, canResetPassword, qaAccount }: Props) { data-test="login-button" > {processing && } - Entrar + {t('Entrar')} {/* @chisel-registration */}
- Ainda não tem conta?{' '} + {t('Ainda não tem conta?')}{' '} - Criar conta + {t('Criar conta')}
{/* @end-chisel-registration */} diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx index 58995f20..bc54aeee 100644 --- a/resources/js/pages/auth/register.tsx +++ b/resources/js/pages/auth/register.tsx @@ -1,4 +1,5 @@ import { participationIdentity } from '@/lib/participation-identity'; +import { useTempoUi } from '@/components/tempo-brand'; import { Form, Head } from '@inertiajs/react'; import InputError from '@/components/input-error'; import PasswordInput from '@/components/password-input'; @@ -15,9 +16,10 @@ type Props = { }; export default function Register({ passwordRules }: Props) { + const { t } = useTempoUi(); return ( <> - +
- +
- + @@ -76,7 +78,7 @@ export default function Register({ passwordRules }: Props) {
{processing && } - Criar conta + {t('Criar conta')}
- Já tem conta?{' '} + {t('Já tem conta?')}{' '} - Entrar + {t('Entrar')}
diff --git a/resources/js/pages/investment/shared.tsx b/resources/js/pages/investment/shared.tsx index 662bb268..78524d6d 100644 --- a/resources/js/pages/investment/shared.tsx +++ b/resources/js/pages/investment/shared.tsx @@ -1,6 +1,6 @@ import { Head, Link, usePage } from '@inertiajs/react'; import type { ReactNode } from 'react'; -import { PortalMenu } from '@/layouts/portal-navigation'; +import { PortalMenu } from '@/components/portal-menu'; import { cn } from '@/lib/utils'; import type { TerritorialSourceEvidence } from '@/components/territorial-source-evidence'; import type { diff --git a/resources/js/pages/portal/article.tsx b/resources/js/pages/portal/article.tsx index 66d11248..d5f0985b 100644 --- a/resources/js/pages/portal/article.tsx +++ b/resources/js/pages/portal/article.tsx @@ -1,5 +1,5 @@ import { Head, Link } from '@inertiajs/react'; -import { PortalMenu } from '@/layouts/portal-navigation'; +import { PortalMenu } from '@/components/portal-menu'; import EditorialReading, { type EditorialArticle, } from '@/components/editorial-reading'; diff --git a/resources/js/pages/portal/document.tsx b/resources/js/pages/portal/document.tsx index b371ef12..761b95ff 100644 --- a/resources/js/pages/portal/document.tsx +++ b/resources/js/pages/portal/document.tsx @@ -1,5 +1,5 @@ import { Head, Link } from '@inertiajs/react'; -import { PortalMenu } from '@/layouts/portal-navigation'; +import { PortalMenu } from '@/components/portal-menu'; import { PublicDocumentContent, type PublicDocument, diff --git a/resources/js/pages/portal/services.tsx b/resources/js/pages/portal/services.tsx index c396455c..c5675f84 100644 --- a/resources/js/pages/portal/services.tsx +++ b/resources/js/pages/portal/services.tsx @@ -6,7 +6,7 @@ import { } from '@/components/public-document-content'; import { Head, Link, useForm, usePage } from '@inertiajs/react'; import { useState } from 'react'; -import { PortalMenu } from '@/layouts/portal-navigation'; +import { PortalMenu } from '@/components/portal-menu'; import { newSubmissionKey } from '@/lib/portal-api'; import { ProvinceSelect } from '@/components/investment-territory'; import { SimulationForm } from '@/components/simulation-form'; diff --git a/resources/js/translation/page-translator.tsx b/resources/js/translation/page-translator.tsx new file mode 100644 index 00000000..e317d860 --- /dev/null +++ b/resources/js/translation/page-translator.tsx @@ -0,0 +1,119 @@ +import { useEffect } from 'react'; +import { useTempoUi } from '@/components/tempo-brand'; +import { translateText } from '@/translation/service'; +import { protectTranslationTokens, restoreTranslationTokens } from '@/translation/utils'; + +// Never change the structure of React's DOM or the value of a form control. +const excluded = 'script,style,noscript,code,pre,textarea,[contenteditable]:not([contenteditable="false"]),[translate="no"],[data-no-translate],.no-translate'; +const attributes = ['placeholder', 'title', 'aria-label', 'alt'] as const; +type Target = { node: Text | Element; attribute?: string; original: string; translated?: string }; + +function read(target: Target): string | null { + return target.attribute ? (target.node as Element).getAttribute(target.attribute) : target.node.nodeValue; +} + +function write(target: Target, value: string): void { + if (target.attribute) (target.node as Element).setAttribute(target.attribute, value); + else target.node.nodeValue = value; +} + +export function PageTranslator() { + const { language } = useTempoUi(); + + useEffect(() => { + if (language === 'pt') return; + let active = true; + const abort = new AbortController(); + const targets = new Map>(); + const cache = new Map>(); + const queue: Array<() => void> = []; + let running = 0; + let timer: ReturnType | undefined; + let unavailable = false; + + const run = () => { + if (!active || unavailable || timer || running >= 2 || !queue.length) return; + running++; + queue.shift()?.(); + // Stay below the API's per-minute limit on large pages. + timer = setTimeout(() => { timer = undefined; run(); }, 650); + }; + const translate = (value: string): Promise => { + const previous = cache.get(value); + if (previous) return previous; + const promise = new Promise((resolve, reject) => { + queue.push(() => { + const protectedText = protectTranslationTokens(value); + translateText({ text: protectedText.text, sourceLanguage: 'auto', targetLanguage: language }, abort.signal) + .then(({ translatedText }) => { + for (const token of protectedText.replacements.keys()) { + if (!translatedText.includes(token)) throw new Error('Translation lost a protected token'); + } + resolve(restoreTranslationTokens(translatedText, protectedText.replacements)); + }) + .catch((error) => { + if (!abort.signal.aborted && (error?.status === 503 || error?.status === null)) unavailable = true; + reject(error); + }) + .finally(() => { running--; run(); }); + }); + run(); + }); + cache.set(value, promise); + promise.catch(() => cache.delete(value)); + return promise; + }; + const consider = (node: Text | Element, attribute?: string) => { + const element = node instanceof Element ? node : node.parentElement; + if (!element || element.closest(excluded)) return; + // Without an explicit value, translating an option changes the submitted value. + if (element.closest('option:not([value])')) return; + const key = attribute ?? ''; + const value = attribute ? (node as Element).getAttribute(attribute) : node.nodeValue; + if (!value || value.length > 10000 || !/\p{L}/u.test(value)) return; + let entries = targets.get(node); + if (!entries) { entries = new Map(); targets.set(node, entries); } + const old = entries.get(key); + if (old && (value === old.original || value === old.translated)) return; + const target: Target = { node, attribute, original: value }; + entries.set(key, target); + translate(value).then((translated) => { + if (!active || !node.isConnected || entries?.get(key) !== target || read(target) !== value) return; + target.translated = translated; + write(target, translated); + }).catch(() => { /* Keep the original visible when translation fails. */ }); + }; + const scan = (root: Node) => { + if (root instanceof Text) { consider(root); return; } + if (!(root instanceof Element)) return; + if (root.closest(excluded)) return; + for (const name of attributes) if (root.hasAttribute(name)) consider(root, name); + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT); + while (walker.nextNode()) { + const node = walker.currentNode; + if (node instanceof Text) consider(node); + else if (node instanceof Element) for (const name of attributes) if (node.hasAttribute(name)) consider(node, name); + } + }; + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'characterData') scan(mutation.target); + else if (mutation.type === 'attributes' && mutation.target instanceof Element && mutation.attributeName) consider(mutation.target, mutation.attributeName); + else mutation.addedNodes.forEach(scan); + } + }); + observer.observe(document.body, { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: [...attributes] }); + scan(document.body); + return () => { + active = false; + abort.abort(); + if (timer) clearTimeout(timer); + observer.disconnect(); + for (const entries of targets.values()) for (const target of entries.values()) { + if (target.translated !== undefined && read(target) === target.translated) write(target, target.original); + } + }; + }, [language]); + + return null; +} diff --git a/resources/js/translation/service.ts b/resources/js/translation/service.ts new file mode 100644 index 00000000..cfede32a --- /dev/null +++ b/resources/js/translation/service.ts @@ -0,0 +1,162 @@ +import type { + TranslateRequest, + TranslateResponse, + TranslationLanguage, +} from "./types"; +import { readLanguageList } from "./utils"; + +const translationApiBaseUrl = "/api"; + +const REQUEST_TIMEOUT_MS = 65_000; +const MAX_ATTEMPTS = 3; + +export class TranslationRequestError extends Error { + status: number | null; + retryable: boolean; + + constructor(message: string, options?: { status?: number | null; retryable?: boolean }) { + super(message); + this.name = "TranslationRequestError"; + this.status = options?.status ?? null; + this.retryable = options?.retryable ?? false; + } +} + +function isRetryableStatus(status: number): boolean { + return [429, 500, 502, 503, 504].includes(status); +} + +async function wait(ms: number): Promise { + await new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +async function fetchJson( + input: RequestInfo | URL, + init: RequestInit = {}, + attempt = 1, + maxAttempts = MAX_ATTEMPTS, +): Promise { + const parentSignal = init.signal; + const controller = new AbortController(); + const timeoutId = window.setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + const forwardAbort = () => controller.abort(); + parentSignal?.addEventListener("abort", forwardAbort, { once: true }); + + try { + const response = await fetch(input, { + ...init, + signal: controller.signal, + headers: { + Accept: "application/json", + ...(init.headers ?? {}), + }, + }); + + if (!response.ok) { + const retryable = isRetryableStatus(response.status); + if (retryable && attempt < maxAttempts) { + await wait(300 * attempt); + return fetchJson(input, init, attempt + 1, maxAttempts); + } + + let message = `Translation request failed with status ${response.status}`; + try { + const payload = (await response.json()) as { message?: string }; + if (payload?.message) { + message = payload.message; + } + } catch { + // Ignore JSON parse issues on error responses. + } + + throw new TranslationRequestError(message, { + status: response.status, + retryable, + }); + } + + return (await response.json()) as T; + } catch (error) { + const aborted = controller.signal.aborted; + const isDomAbort = error instanceof DOMException && error.name === "AbortError"; + const parentAborted = parentSignal?.aborted ?? false; + const timedOut = aborted && !parentAborted; + const retryable = timedOut || error instanceof TypeError; + + if (retryable && attempt < maxAttempts) { + await wait(300 * attempt); + return fetchJson(input, init, attempt + 1, maxAttempts); + } + + if (error instanceof TranslationRequestError) { + throw error; + } + + if (parentAborted || isDomAbort) { + throw error; + } + + if (timedOut) { + throw new TranslationRequestError("Translation request timed out", { + status: null, + retryable, + }); + } + + throw new TranslationRequestError("Translation request failed", { + status: null, + retryable, + }); + } finally { + window.clearTimeout(timeoutId); + parentSignal?.removeEventListener("abort", forwardAbort); + } +} + +export async function getLanguages(signal?: AbortSignal): Promise { + const payload = await fetchJson(`${translationApiBaseUrl}/translate/languages`, { + method: "GET", + signal, + }, 1, 1); + + return readLanguageList(payload); +} + +export async function translateText( + request: TranslateRequest, + signal?: AbortSignal, +): Promise { + const payload = await fetchJson<{ + original_text?: string; + translated_text?: string; + source_lang?: string; + target_lang?: string; + }>(`${translationApiBaseUrl}/translate`, { + method: "POST", + signal, + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + text: request.text, + source_lang: request.sourceLanguage, + target_lang: request.targetLanguage, + is_html: request.isHtml ?? false, + }), + }, 1, 1); + + if (typeof payload.translated_text !== "string" || payload.translated_text.trim() === "") { + throw new TranslationRequestError("Translation response is missing translated_text", { + status: 200, + retryable: false, + }); + } + + return { + originalText: payload.original_text ?? request.text, + translatedText: payload.translated_text, + sourceLanguage: payload.source_lang ?? request.sourceLanguage, + targetLanguage: payload.target_lang ?? request.targetLanguage, + }; +} diff --git a/resources/js/translation/types.ts b/resources/js/translation/types.ts new file mode 100644 index 00000000..20bd534d --- /dev/null +++ b/resources/js/translation/types.ts @@ -0,0 +1,41 @@ +export interface TranslationLanguage { + code: string; + name: string; + targets?: string[]; +} + +export interface TranslateRequest { + text: string; + sourceLanguage: string; + targetLanguage: string; + isHtml?: boolean; +} + +export interface TranslateResponse { + originalText: string; + translatedText: string; + sourceLanguage: string; + targetLanguage: string; +} + +export interface TranslationCacheEntry { + value: string; + createdAt: number; + expiresAt: number; +} + +export interface ProtectedTranslationTokens { + text: string; + replacements: Map; +} + +export interface TranslationPayloadHealth { + shouldTranslate: boolean; + normalizedText: string; +} + +export interface TranslateOptions { + isHtml?: boolean; + signal?: AbortSignal; + protectedTerms?: string[]; +} diff --git a/resources/js/translation/utils.ts b/resources/js/translation/utils.ts new file mode 100644 index 00000000..02c6c951 --- /dev/null +++ b/resources/js/translation/utils.ts @@ -0,0 +1,615 @@ +import type { + ProtectedTranslationTokens, + TranslationPayloadHealth, + TranslationCacheEntry, + TranslationLanguage, +} from "./types"; + +const CACHE_PREFIX = "tempo_translation_v1"; +const CACHE_VERSION = "v1"; +const CACHE_TTL = 7 * 24 * 60 * 60 * 1000; +const CACHE_MAX_ENTRIES = 200; +const CACHE_CLEANUP_INTERVAL = 12 * 60 * 60 * 1000; +const CLEANUP_MARKER_KEY = `${CACHE_PREFIX}:cleanup`; +const memoryCache = new Map(); + +const LANGUAGE_ALIASES: Record = { + ar: "ar", + "ar-sa": "ar", + en: "en", + "en-gb": "en", + "en-us": "en", + es: "es", + "es-es": "es", + fr: "fr", + "fr-fr": "fr", + hi: "hi", + "hi-in": "hi", + pt: "pt", + "pt-mz": "pt", + "pt-pt": "pt", + zh: "zh-Hans", + "zh-cn": "zh-Hans", + "zh-hans": "zh-Hans", +}; + +const apiUnsafeNodes = "script, iframe, object, embed, link, meta"; +const excludedTranslationNodes = + "code, pre, script, style, textarea, noscript, .no-translate, [data-no-translate], [translate='no']"; +const INTERNAL_PLACEHOLDER_PREFIX = "¤TRX"; +const BATCH_MAX_ITEMS = 8; +const BATCH_MAX_CHARS = 2200; +const TRANSLATION_PLACEHOLDER_PATTERN = /¤TRX[A-Z]+\d+¤/g; +const HTML_BATCH_ROOT_ATTR = "data-trx-batch-root"; +const HTML_BATCH_NODE_ATTR = "data-trx-node"; + +export const fallbackTranslationLanguages: TranslationLanguage[] = [ + { code: "pt", name: "Português" }, + { code: "en", name: "English" }, + { code: "fr", name: "French" }, + { code: "es", name: "Spanish" }, + { code: "ar", name: "Arabic" }, + { code: "hi", name: "Hindi" }, + { code: "zh-Hans", name: "Chinese" }, +]; + +export const DEFAULT_PROTECTED_TERMS = [ + "Revista TEMPO", + "Grupo TEMPO", + "Tempo TV", + "Tempo Connect", + "Informação Pública", + "InvesteMoz", + "TEMPO", +] as const; + +function getStorage(): Storage | null { + if (typeof window === "undefined") return null; + return window.localStorage; +} + +function normalizeAlias(code: string): string { + return LANGUAGE_ALIASES[code.trim().toLowerCase()] ?? code.trim(); +} + +export function normalizeLanguageCode( + code: string, + availableLanguages: TranslationLanguage[], +): string | null { + if (!code) return null; + + const normalized = normalizeAlias(code); + const supportedCodes = new Set( + availableLanguages.map((language) => normalizeAlias(language.code)), + ); + + return supportedCodes.has(normalized) ? normalized : null; +} + +export function getBrowserLanguageCode(code: string): string { + switch (code) { + case "zh-Hans": + return "zh-CN"; + default: + return code; + } +} + +export function getLocaleForLanguage(code: string): string { + switch (normalizeAlias(code)) { + case "pt": + return "pt-MZ"; + case "zh-Hans": + return "zh-CN"; + default: + return normalizeAlias(code); + } +} + +export function isRtlLanguage(code: string): boolean { + return normalizeAlias(code) === "ar"; +} + +export function readLanguageList(payload: unknown): TranslationLanguage[] { + const source = Array.isArray(payload) + ? payload + : Array.isArray((payload as { value?: unknown[] })?.value) + ? (payload as { value: unknown[] }).value + : Array.isArray((payload as { data?: unknown[] })?.data) + ? (payload as { data: unknown[] }).data + : []; + + return source + .filter((item): item is TranslationLanguage => { + return ( + Boolean(item) && + typeof item === "object" && + typeof item.code === "string" && + typeof item.name === "string" + ); + }) + .map((language) => ({ + code: normalizeAlias(language.code), + name: language.name, + targets: Array.isArray(language.targets) + ? language.targets.map((target) => normalizeAlias(target)) + : undefined, + })); +} + +export function withPortugueseFirst( + languages: TranslationLanguage[], +): TranslationLanguage[] { + const unique = new Map(); + + for (const language of languages) { + const code = normalizeAlias(language.code); + if (!unique.has(code)) { + unique.set(code, { + ...language, + code, + name: code === "pt" ? "Português" : language.name, + }); + } + } + + const items = Array.from(unique.values()).sort((a, b) => + a.name.localeCompare(b.name), + ); + + const portuguese = + items.find((language) => language.code === "pt") ?? fallbackTranslationLanguages[0]; + + return [portuguese, ...items.filter((language) => language.code !== "pt")]; +} + +export function hashTranslationText(value: string): string { + let hash = 2166136261; + + for (let index = 0; index < value.length; index += 1) { + hash ^= value.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + + return (hash >>> 0).toString(16); +} + +export function createTranslationCacheKey(params: { + sourceLanguage: string; + targetLanguage: string; + isHtml: boolean; + text: string; + protectedTerms?: string[]; +}): string { + return [ + CACHE_PREFIX, + CACHE_VERSION, + params.sourceLanguage, + params.targetLanguage, + params.isHtml ? "html" : "text", + hashTranslationText(params.text), + hashTranslationText( + JSON.stringify( + collectProtectedTerms(params.protectedTerms).map((term) => term.toLowerCase()), + ), + ), + ].join(":"); +} + +export function getMemoryCacheEntry(key: string): TranslationCacheEntry | null { + const entry = memoryCache.get(key); + if (!entry) return null; + + if (entry.expiresAt <= Date.now()) { + memoryCache.delete(key); + return null; + } + + return entry; +} + +export function setMemoryCacheEntry(key: string, value: string): TranslationCacheEntry { + const entry: TranslationCacheEntry = { + value, + createdAt: Date.now(), + expiresAt: Date.now() + CACHE_TTL, + }; + + memoryCache.set(key, entry); + return entry; +} + +export function getPersistentCacheEntry(key: string): TranslationCacheEntry | null { + const storage = getStorage(); + if (!storage) return null; + + const raw = storage.getItem(key); + if (!raw) return null; + + try { + const entry = JSON.parse(raw) as TranslationCacheEntry; + if (entry.expiresAt <= Date.now()) { + storage.removeItem(key); + return null; + } + + return entry; + } catch { + storage.removeItem(key); + return null; + } +} + +export function setPersistentCacheEntry(key: string, entry: TranslationCacheEntry): void { + const storage = getStorage(); + if (!storage) return; + + storage.setItem(key, JSON.stringify(entry)); +} + +export function cleanupTranslationCache(): void { + const storage = getStorage(); + if (!storage) return; + + const lastCleanup = Number(storage.getItem(CLEANUP_MARKER_KEY) ?? "0"); + if (lastCleanup && Date.now() - lastCleanup < CACHE_CLEANUP_INTERVAL) { + return; + } + + const translationKeys: string[] = []; + + for (let index = 0; index < storage.length; index += 1) { + const key = storage.key(index); + if (key && key.startsWith(`${CACHE_PREFIX}:${CACHE_VERSION}:`)) { + translationKeys.push(key); + } + } + + const validEntries: Array<{ key: string; entry: TranslationCacheEntry }> = []; + + for (const key of translationKeys) { + const entry = getPersistentCacheEntry(key); + if (entry) { + validEntries.push({ key, entry }); + } + } + + validEntries + .sort((a, b) => a.entry.createdAt - b.entry.createdAt) + .slice(0, Math.max(0, validEntries.length - CACHE_MAX_ENTRIES)) + .forEach(({ key }) => storage.removeItem(key)); + + storage.setItem(CLEANUP_MARKER_KEY, String(Date.now())); +} + +function createPlaceholder(prefix: string, index: number): string { + return `${INTERNAL_PLACEHOLDER_PREFIX}${prefix}${index}¤`; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +export function collectProtectedTerms(terms?: Array): string[] { + const combined = [...DEFAULT_PROTECTED_TERMS, ...(terms ?? [])] + .map((term) => String(term ?? "").trim()) + .filter(Boolean); + + return Array.from(new Set(combined)).sort((a, b) => b.length - a.length); +} + +function protectMatches( + input: string, + pattern: RegExp, + replacements: Map, + prefix: string, +): string { + return input.replace(pattern, (match) => { + const placeholder = createPlaceholder(prefix, replacements.size); + replacements.set(placeholder, match); + return placeholder; + }); +} + +function protectExcludedHtmlBlocks( + input: string, + replacements: Map, +): string { + if (typeof window === "undefined") return input; + + const documentFragment = new DOMParser().parseFromString(input, "text/html"); + const nodes = Array.from(documentFragment.body.querySelectorAll(excludedTranslationNodes)); + + nodes.forEach((node) => { + const placeholder = createPlaceholder("HTML", replacements.size); + replacements.set(placeholder, node.outerHTML); + node.replaceWith(documentFragment.createTextNode(placeholder)); + }); + + return documentFragment.body.innerHTML; +} + +export function protectTranslationTokens( + input: string, + options?: { isHtml?: boolean; protectedTerms?: string[] }, +): ProtectedTranslationTokens { + const replacements = new Map(); + let output = String(input ?? ""); + + if (options?.isHtml) { + output = protectExcludedHtmlBlocks(output, replacements); + } + + output = protectMatches( + output, + /https?:\/\/[^\s"'<>]+/gi, + replacements, + "URL", + ); + output = protectMatches( + output, + /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, + replacements, + "EMAIL", + ); + output = protectMatches( + output, + /\{\{[^{}]+\}\}|\{[a-zA-Z0-9_.-]+\}|%(\d+\$)?[sd]|:[a-zA-Z_][a-zA-Z0-9_]*/g, + replacements, + "TOKEN", + ); + + for (const term of collectProtectedTerms(options?.protectedTerms)) { + const escapedTerm = escapeRegExp(term); + const pattern = new RegExp(`(^|[^\\p{L}\\p{N}])(${escapedTerm})(?=$|[^\\p{L}\\p{N}])`, "giu"); + + output = output.replace(pattern, (match, prefix, protectedValue) => { + const placeholder = createPlaceholder("TERM", replacements.size); + replacements.set(placeholder, protectedValue); + return `${prefix}${placeholder}`; + }); + } + + return { text: output, replacements }; +} + +export function restoreTranslationTokens( + input: string, + replacements: Map, +): string { + let output = String(input ?? ""); + + for (const [placeholder, original] of replacements.entries()) { + output = output.split(placeholder).join(original); + } + + return output; +} + +export function getTranslationPayloadHealth( + input: string, + options?: { isHtml?: boolean; protectedTerms?: string[] }, +): TranslationPayloadHealth { + const source = String(input ?? ""); + if (!source.trim()) { + return { + shouldTranslate: false, + normalizedText: source, + }; + } + + if (options?.isHtml && typeof window !== "undefined") { + const documentFragment = new DOMParser().parseFromString(source, "text/html"); + const walker = document.createTreeWalker( + documentFragment.body, + NodeFilter.SHOW_TEXT, + { + acceptNode(node) { + return isSkippableTextNode(node as Text) + ? NodeFilter.FILTER_REJECT + : NodeFilter.FILTER_ACCEPT; + }, + }, + ); + + if (!walker.nextNode()) { + return { + shouldTranslate: false, + normalizedText: source, + }; + } + } + + const protectedPayload = protectTranslationTokens(source, options); + const remainder = protectedPayload.text + .replace(TRANSLATION_PLACEHOLDER_PATTERN, "") + .trim(); + + if (!remainder) { + return { + shouldTranslate: false, + normalizedText: source, + }; + } + + const hasLetters = /[\p{L}]/u.test(remainder); + if (!hasLetters) { + return { + shouldTranslate: false, + normalizedText: source, + }; + } + + return { + shouldTranslate: true, + normalizedText: source, + }; +} + +export function sanitizeTranslatedHtml(html: string): string { + if (typeof window === "undefined" || !html) return ""; + + const documentFragment = new DOMParser().parseFromString(String(html), "text/html"); + documentFragment.querySelectorAll(apiUnsafeNodes).forEach((node) => node.remove()); + + documentFragment.querySelectorAll("*").forEach((node) => { + for (const attribute of Array.from(node.attributes)) { + const name = attribute.name.toLowerCase(); + const value = attribute.value.trim().toLowerCase(); + + if ( + name.startsWith("on") || + value.startsWith("javascript:") || + value.startsWith("data:text/html") || + value.startsWith("vbscript:") + ) { + node.removeAttribute(attribute.name); + } + } + }); + + return documentFragment.body.innerHTML; +} + +function isSkippableTextNode(node: Text): boolean { + const parent = node.parentElement; + const value = node.textContent ?? ""; + + if (!value.trim()) return true; + if (!parent) return true; + if (parent.closest(excludedTranslationNodes)) return true; + + return false; +} + +function createBatchPayload(values: string[]): { + payload: string; +} { + if (typeof window === "undefined") { + return { + payload: values.join(" "), + }; + } + + const documentFragment = document.implementation.createHTMLDocument(""); + const root = documentFragment.createElement("div"); + root.setAttribute(HTML_BATCH_ROOT_ATTR, "1"); + + values.forEach((value, index) => { + const node = documentFragment.createElement("span"); + node.setAttribute(HTML_BATCH_NODE_ATTR, String(index)); + node.textContent = value; + root.appendChild(node); + }); + + documentFragment.body.appendChild(root); + + return { payload: root.outerHTML }; +} + +function parseBatchResponse( + translated: string, + originalValues: string[], +): string[] { + if (typeof window === "undefined") { + return originalValues; + } + + const documentFragment = new DOMParser().parseFromString(translated, "text/html"); + const root = documentFragment.body.querySelector(`[${HTML_BATCH_ROOT_ATTR}]`); + if (!root) { + return originalValues; + } + + const nodes = Array.from(root.querySelectorAll(`[${HTML_BATCH_NODE_ATTR}]`)); + if (nodes.length !== originalValues.length) { + return originalValues; + } + + return originalValues.map((originalValue, index) => { + const node = nodes.find( + (candidate) => candidate.getAttribute(HTML_BATCH_NODE_ATTR) === String(index), + ); + + return node?.textContent?.trim() ? node.textContent : originalValue; + }); +} + +function buildTextBatches(nodes: Text[]): Text[][] { + const batches: Text[][] = []; + let currentBatch: Text[] = []; + let currentLength = 0; + + for (const node of nodes) { + const textLength = (node.textContent ?? "").length; + const shouldFlush = + currentBatch.length >= BATCH_MAX_ITEMS || + currentLength + textLength > BATCH_MAX_CHARS; + + if (shouldFlush && currentBatch.length > 0) { + batches.push(currentBatch); + currentBatch = []; + currentLength = 0; + } + + currentBatch.push(node); + currentLength += textLength; + } + + if (currentBatch.length > 0) { + batches.push(currentBatch); + } + + return batches; +} + +export async function translateHtmlByTextNodes(params: { + html: string; + protectedTerms?: string[]; + translateText: (text: string, options?: { protectedTerms?: string[]; isHtml?: boolean }) => Promise; +}): Promise { + if (typeof window === "undefined" || !params.html.trim()) { + return params.html; + } + + const documentFragment = new DOMParser().parseFromString(params.html, "text/html"); + const walker = document.createTreeWalker( + documentFragment.body, + NodeFilter.SHOW_TEXT, + { + acceptNode(node) { + return isSkippableTextNode(node as Text) + ? NodeFilter.FILTER_REJECT + : NodeFilter.FILTER_ACCEPT; + }, + }, + ); + + const textNodes: Text[] = []; + let currentNode = walker.nextNode(); + while (currentNode) { + textNodes.push(currentNode as Text); + currentNode = walker.nextNode(); + } + + if (textNodes.length === 0) { + return sanitizeTranslatedHtml(documentFragment.body.innerHTML); + } + + for (const batch of buildTextBatches(textNodes)) { + const originalValues = batch.map((node) => node.textContent ?? ""); + const { payload } = createBatchPayload(originalValues); + const translatedPayload = await params.translateText(payload, { + protectedTerms: params.protectedTerms, + isHtml: true, + }); + + const translatedValues = parseBatchResponse(translatedPayload, originalValues); + + batch.forEach((node, index) => { + node.textContent = translatedValues[index]; + }); + } + + return sanitizeTranslatedHtml(documentFragment.body.innerHTML); +} diff --git a/routes/api.php b/routes/api.php index 5278aca3..07b49d34 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ middleware('throttle:60,1'); +Route::post('translate', [TranslationApiController::class, 'translate'])->middleware('throttle:120,1'); + Route::prefix('v1')->middleware(['throttle:api', CorrelationId::class])->group(function () { Route::get('/catalogs/{catalog}', [ReferenceCatalogController::class, 'index']) ->whereIn('catalog', ['authorities', 'jurisdictions', 'economic-activities', 'countries']); diff --git a/tests/Feature/TranslationApiTest.php b/tests/Feature/TranslationApiTest.php new file mode 100644 index 00000000..684a044d --- /dev/null +++ b/tests/Feature/TranslationApiTest.php @@ -0,0 +1,27 @@ + 'http://translator.test']); + Http::fake(['translator.test/languages' => Http::response([['code' => 'pt', 'name' => 'Portuguese', 'targets' => ['en']]])]); + + $this->getJson('/api/translate/languages')->assertOk()->assertJsonPath('0.targets.0', 'en'); + } + + public function test_it_translates_and_reports_engine_failures(): void + { + config(['services.translation.url' => 'http://translator.test']); + Http::fake(['translator.test/translate' => Http::sequence()->push(['translatedText' => 'Hello'])->push([], 500)]); + $input = ['text' => 'Olá', 'source_lang' => 'pt', 'target_lang' => 'en']; + + $this->postJson('/api/translate', $input)->assertOk()->assertJsonPath('translated_text', 'Hello'); + $this->postJson('/api/translate', $input)->assertStatus(503); + } +} From c5756c0282df5ce500ff69fa30eb09e853e7cbd7 Mon Sep 17 00:00:00 2001 From: Paulo Massingue Date: Wed, 4 Nov 2026 12:38:21 +0200 Subject: [PATCH 2/3] pesquisa avancada no portal --- .../Controllers/GlobalSearchController.php | 173 ++++++++++++++++++ app/Services/FederatedSearch.php | 78 ++++++++ app/Services/SearchTerms.php | 112 ++++++++++++ .../unification/traceability-and-manual.md | 17 ++ .../v2/auditoria-portal/matriz-requisitos.csv | 2 +- resources/js/components/portal-hero.tsx | 6 +- resources/js/pages/PortalInvestidor.tsx | 2 +- resources/js/pages/portal/fiscal-forms.tsx | 2 +- resources/js/pages/portal/search.tsx | 54 ++++++ routes/web.php | 1 + tests/Feature/GlobalSearchTest.php | 55 ++++++ tests/Unit/FederatedSearchTest.php | 47 +++++ tests/Unit/SearchTermsTest.php | 59 ++++++ tests/frontend/fiscal-form-entry.test.mjs | 2 +- tests/frontend/global-search.test.mjs | 36 ++++ 15 files changed, 639 insertions(+), 7 deletions(-) create mode 100644 app/Http/Controllers/GlobalSearchController.php create mode 100644 app/Services/FederatedSearch.php create mode 100644 app/Services/SearchTerms.php create mode 100644 resources/js/pages/portal/search.tsx create mode 100644 tests/Feature/GlobalSearchTest.php create mode 100644 tests/Unit/FederatedSearchTest.php create mode 100644 tests/Unit/SearchTermsTest.php create mode 100644 tests/frontend/global-search.test.mjs 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/); +}); From 705dc69d683a451eda1371db7eceb280047b2895 Mon Sep 17 00:00:00 2001 From: Paulo Massingue Date: Wed, 4 Nov 2026 15:52:55 +0200 Subject: [PATCH 3/3] pesquisa intelignte --- resources/js/components/portal-hero.tsx | 4 ++-- resources/js/pages/PortalInvestidor.tsx | 5 ----- routes/web.php | 1 + tests/frontend/quick-opportunity-search.test.mjs | 8 ++++---- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/resources/js/components/portal-hero.tsx b/resources/js/components/portal-hero.tsx index a2cc918f..400636d5 100644 --- a/resources/js/components/portal-hero.tsx +++ b/resources/js/components/portal-hero.tsx @@ -226,14 +226,14 @@ export default function PortalHero({

{ event.preventDefault(); const value = new FormData(event.currentTarget).get( 'q', ); - router.get('/oportunidades', { + router.get('/pesquisa', { q: typeof value === 'string' ? value.trim() diff --git a/resources/js/pages/PortalInvestidor.tsx b/resources/js/pages/PortalInvestidor.tsx index e28c46b9..908a138a 100644 --- a/resources/js/pages/PortalInvestidor.tsx +++ b/resources/js/pages/PortalInvestidor.tsx @@ -971,11 +971,6 @@ export default function PortalInvestidorApp({ router.get('/oportunidades'); } - function handleHeroSearch(event: FormEvent) { - event.preventDefault(); - router.get('/oportunidades', { q: heroQuery.trim() }); - } - function handleView(opportunity: Opportunity) { router.get('/oportunidades/' + encodeURIComponent(opportunity.slug)); } diff --git a/routes/web.php b/routes/web.php index c4ef53ec..02af514b 100644 --- a/routes/web.php +++ b/routes/web.php @@ -115,6 +115,7 @@ Route::get('/artigos/{id}', [EditorialArticleController::class, 'show'])->whereN 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/frontend/quick-opportunity-search.test.mjs b/tests/frontend/quick-opportunity-search.test.mjs index 96dc30bf..31283ba7 100644 --- a/tests/frontend/quick-opportunity-search.test.mjs +++ b/tests/frontend/quick-opportunity-search.test.mjs @@ -55,7 +55,7 @@ await test('the quick search has a native GET destination and submits the actual const input = nodes(tree).find( (node) => node.type === 'input' && node.props.name === 'q', ); - assert.equal(form.props.action, '/oportunidades'); + assert.equal(form.props.action, '/pesquisa'); assert.equal(form.props.method, 'get'); assert.equal(input.props.maxLength, 120); input.props.onChange({ target: { value: 'plantio de algodao' } }); @@ -80,15 +80,15 @@ await test('the quick search has a native GET destination and submits the actual }); assert.equal(prevented, true); assert.deepEqual(sent, { - url: '/oportunidades', + url: '/pesquisa', data: { q: 'plantio de algodao' }, }); } finally { globalThis.FormData = original; } const html = renderToStaticMarkup(tree); - assert.ok(html.includes('Pesquisa rápida de oportunidades')); - assert.ok(html.includes('action="/oportunidades"')); + assert.ok(html.includes('Pesquisa rápida no InvesteMoz')); + assert.ok(html.includes('action="/pesquisa"')); assert.ok(html.includes('Pesquisar')); });