From f3455432cf77976312bd9066bf7fdcc9a2503884 Mon Sep 17 00:00:00 2001 From: Paulo Massingue Date: Tue, 3 Nov 2026 15:31:25 +0200 Subject: [PATCH] . --- .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); + } +}