Compare commits

..
20 changed files with 289 additions and 1554 deletions
@@ -1,27 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\EconomicCluster;
use App\Services\ClusterCompetitiveness;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
final class ClusterCompetitivenessController extends Controller
{
public function show(EconomicCluster $cluster, ClusterCompetitiveness $service): JsonResponse
{
return response()->json($service->management($cluster->id))->header('Cache-Control', 'private, no-store');
}
public function change(Request $request, EconomicCluster $cluster, string $action, ClusterCompetitiveness $service): JsonResponse
{
$data = $request->validate(['expected_version' => 'required|integer|min:0',
'draft' => $action === 'saved' ? 'required|array:version,clusterId,sector,region,forces' : 'prohibited',
'revision' => $action === 'restored' ? 'required|integer|min:1' : 'prohibited',
'reason' => in_array($action, ['approved', 'withdrawn'], true) ? 'required|string|min:10|max:3000' : 'nullable|string|max:3000']);
return response()->json($service->change($cluster, $request->user()->id, (int) $data['expected_version'], $action, $data))
->header('Cache-Control', 'private, no-store');
}
}
@@ -140,7 +140,6 @@ class InvestmentPortfolioController extends Controller
return Inertia::render('portfolio/show', ['complements' => $management ? [] : app(\App\Services\InvestmentComplements::class)->options('portfolio:'.$record->id), 'record' => $record, 'related' => $related, 'management' => $management, 'zones' => $zones,
'anchorProject' => $anchorProject, 'existingSubmissions' => $existingSubmissions,
'competitiveness' => $record->cluster_id ? app(\App\Services\ClusterCompetitiveness::class)->publicData($record->cluster_id) : null,
'geography' => app(InvestmentRelations::class)->dossier($record, $management),
'origin' => $origin ? ['id' => $origin->id, 'title' => $origin->title] : null, 'opportunities' => $opportunities, 'chains' => $chains]);
}
@@ -50,7 +50,6 @@ class InvestmentStructureController extends Controller
->whereNull('origins.deleted_at')->whereNull('destinations.deleted_at')->whereNull('value_chain_links.deleted_at')
->orderBy('origins.code')->orderBy('destinations.code')
->get(['value_chain_links.id', 'origins.name as origin', 'destinations.name as destination', 'link_type']) : [],
'competitiveness' => $structure === 'clusters' && $id ? app(\App\Services\ClusterCompetitiveness::class)->management($id) : null,
'viewerId' => $request->user()->id,
'memberships' => $structure === 'clusters' && $id ? DB::table('cluster_projects')
->join('projects', 'projects.id', '=', 'cluster_projects.project_id')
-130
View File
@@ -1,130 +0,0 @@
<?php
namespace App\Services;
use App\Models\EconomicCluster;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
final class ClusterCompetitiveness
{
public const FORCES = ['rivalry', 'entrants', 'substitutes', 'suppliers', 'buyers'];
public const FIELDS = ['evidence', 'source', 'period', 'rationale', 'limitations', 'opportunity'];
private const SOURCE = 'cluster-competitiveness';
private function events(string $cluster): Builder
{
return DB::table('audit_events')->where('source_system', self::SOURCE)
->where('entity_type', 'economic_clusters')->where('entity_id', $cluster);
}
private function decode(?object $event): array
{
return $event ? json_decode(Crypt::decryptString(json_decode($event->after_data, true, 512, JSON_THROW_ON_ERROR)['snapshot']), true, 512, JSON_THROW_ON_ERROR)
: ['version' => 0, 'draft' => null, 'approved' => null];
}
public function current(string $cluster): array
{
return $this->decode($this->events($cluster)->orderByRaw("(after_data->>'version')::integer DESC")->first());
}
public function management(string $cluster): array
{
$state = $this->current($cluster);
$state['history'] = $this->events($cluster)->orderByRaw("(after_data->>'version')::integer DESC")->limit(50)->get()
->map(fn ($event) => ['version' => json_decode($event->after_data, true)['version'],
'action' => $event->action, 'at' => $event->occurred_at, 'actor' => $event->actor_user_id])->all();
return $state;
}
public function validateDraft(array $draft, string $cluster): array
{
$rules = ['version' => 'required|integer|in:1', 'clusterId' => 'required|string|in:'.$cluster,
'sector' => 'present|nullable|string|max:200', 'region' => 'present|nullable|string|max:1000',
'forces' => 'required|array:'.implode(',', self::FORCES)];
foreach (self::FORCES as $force) {
$rules["forces.$force"] = 'required|array:pressure,'.implode(',', self::FIELDS);
$rules["forces.$force.pressure"] = 'present|nullable|in:low,medium,high';
foreach (self::FIELDS as $field) {
$rules["forces.$force.$field"] = 'present|nullable|string|max:5000';
}
}
$clean = Validator::make($draft, $rules)->validate();
$clean['version'] = 1;
$clean['sector'] = trim($clean['sector'] ?? '');
$clean['region'] = trim($clean['region'] ?? '');
foreach (self::FORCES as $force) {
foreach (['pressure', ...self::FIELDS] as $field) {
$clean['forces'][$force][$field] = trim($clean['forces'][$force][$field] ?? '');
}
}
return $clean;
}
private function assertComplete(?array $draft): void
{
$missing = ! $draft || ! $draft['sector'] || ! $draft['region'];
foreach (self::FORCES as $force) {
$row = $draft['forces'][$force] ?? [];
$missing = $missing || ! in_array($row['pressure'] ?? '', ['low', 'medium', 'high'], true);
foreach (self::FIELDS as $field) {
$missing = $missing || trim($row[$field] ?? '') === '';
}
}
if ($missing) {
throw ValidationException::withMessages(['approval' => 'Complete o âmbito e as evidências, fontes, períodos, justificação, limitações e oportunidades das cinco forças antes de aprovar.']);
}
}
/** Reuse the portal's encrypted, append-only audit snapshots; no new schema or mutable publication fields. */
public function change(EconomicCluster $cluster, int $actor, int $expected, string $action, array $input = []): array
{
return DB::transaction(function () use ($cluster, $actor, $expected, $action, $input) {
EconomicCluster::whereKey($cluster->id)->lockForUpdate()->firstOrFail();
$before = $this->current($cluster->id);
abort_if($before['version'] !== $expected, 409, 'A análise foi alterada. Conserve os campos actuais e compare a versão do servidor antes de voltar a guardar.');
$state = $before;
if ($action === 'saved') {
$state['draft'] = $this->validateDraft($input['draft'], $cluster->id);
} elseif ($action === 'restored') {
$event = $this->events($cluster->id)->where('after_data->version', $input['revision'])->first();
abort_unless($event, 404);
$state['draft'] = $this->decode($event)['draft'];
} elseif ($action === 'approved') {
$this->assertComplete($state['draft']);
$state['approved'] = ['draft' => $state['draft'], 'at' => now()->toISOString(), 'actor' => $actor,
'version' => $expected + 1, 'evidence' => $input['reason']];
} elseif ($action === 'withdrawn') {
$state['approved'] = null;
} else {
throw new \LogicException('Unsupported competitiveness action');
}
$state['version']++;
DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'economic_clusters',
'entity_id' => $cluster->id, 'external_id' => $cluster->id.':'.$state['version'], 'actor_user_id' => $actor,
'action' => $action, 'data_classification' => 'restricted',
'before_data' => json_encode(['version' => $before['version']], JSON_THROW_ON_ERROR),
'after_data' => json_encode(['version' => $state['version'], 'restored_from' => $input['revision'] ?? null,
'reason' => Crypt::encryptString($input['reason'] ?? ''),
'snapshot' => Crypt::encryptString(json_encode($state, JSON_THROW_ON_ERROR))], JSON_THROW_ON_ERROR)]);
return $this->management($cluster->id);
});
}
public function publicData(string $cluster): ?array
{
$approved = $this->current($cluster)['approved'];
// Explicit projection: never return editor identity, approval evidence, history or the unapproved draft.
return $approved ? ['draft' => $approved['draft'], 'version' => $approved['version'], 'at' => $approved['at']] : null;
}
}
@@ -1828,39 +1828,15 @@ Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
## 29/10/2026 — ACT-20261029-01, inventário antes da verificação do incremento
## 28/10/2026 — formulários fiscais 332c16eb, QA concluída e autenticação pendente
Continuação sequencial solicitada pela Tempo. Correspondência: ACT-20261029-01 reutiliza D-02 §8, clusters/cadeias e a orientação metodológica documentada em 23/10. Concluído previamente e preservado: gestão de clusters, etapas, ligações, lacunas e relações auditadas; disponibilidade instalada de carteira/âncoras e formulários fiscais conforme recibos de 28/10. Nenhuma perda nova confirmada nesta inspecção de código; não foi feita nova comparação de dados ou alegada prova do servidor. A pasta principal está em fde644af, enquanto o último recibo identifica 332c16eb instalado; reconciliar o delta sobre essa origem antes de instalar, sem substituir o destino pela pasta principal.
Documentos e Formulários disponibiliza no candidato entradas directas IVA/IRPC: identificação, período, 27 campos manuais de Modelo A ou 24 campos manuais de M22, revisão, impressão, ficheiro privado e recuperação com confirmação antes de substituir. Sem geração de zeros, arredondamento, apuramento ou submissão à AT. IRPC permanece parcial: outros ajustamentos, regimes e anexos não incluídos; L8 parcial. Persistência no servidor e associação automática ao dossier não incluídas. Cinco testes frontend e um teste Feature/20 asserções passaram na base QA isolada 127.0.0.1:55441/proactive_investment_20261027_test; tipos, compilação, sintaxe PHP e diff conferidos. Não equivalem a validação visual autenticada. Relatório: docs/investmoz/unification/fiscal-form-preparation-qa-20261028.json.
Incremento em preparação: diagnóstico qualitativo das cinco forças na edição do cluster, sector/região editáveis, fonte/período/evidência/justificação/limites/oportunidade por força; indeterminação explícita, ficheiro privado transferível e recuperação com confirmação. Sem alteração de schema, dados, permissões, publicações ou motores fiscais. Persistência no servidor, ligação territorial canónica, dados sectoriais fundamentados e consulta pública permanecem por implementar; ACT-01 não está concluída. Verificação dirigida e instalação ainda pendentes neste registo inicial. Restantes ACT-02 a 16 mantêm o estado anterior e a sequência, sem trabalho SGF nesta passagem.
### ACT-20261029-01 — candidato sequencial preparado, 29/10/2026
Candidato publicado apenas no ramo Git codex/fiscal-forms-qa-20261028; main e activo não alterados. Instalação anterior 789fffd0 preservada. Preparação de entrega em outputs/fiscal-forms-delivery-20261028, com guarda do destino, backup fresco, restauro isolado e pipeline existentes; execução remota depende de nova autenticação SSH, pois a sessão anterior foi encerrada.
Primeiro incremento implementado, **parcial e ainda não instalado**: gestão → clusters → editar → Competitividade. As cinco forças conservam fonte, período, evidência, apreciação, limites e oportunidade; sector e região explícitos, sem inferências automáticas. Transferência de rascunhos JSON e recuperação restrita ao mesmo cluster, com confirmação antes de substituir. Quatro testes frontend dirigidos, TypeScript e compilação QA passaram tanto na pasta de trabalho como no candidato baseado em 332c16eb. Guarda C132 local passou; não foram efectuados testes nem escritas na base activa. A edição existente recebe apenas a inclusão do componente; contratos e permissões existentes preservados.
Origem reconciliada: main remoto e revisão pública conferidos em 332c16eb em 29/10. Candidato em C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-sequential-20261029. Consulta do estado remoto não equivale a inventário novo da base. Sessão SSH anterior encerrada; autenticação por chave recusada. Instalação requer autenticação SSH, backup fresco, restauro isolado, guarda de versão, pipeline existente e conferência posterior. Não instalar a pasta principal sobre o destino. A sequência solicitada mantém ACT-02 a 16 por iniciar após a instalação e o saldo de ACT-01; persistência no servidor, cobertura factual e consulta pública continuam pendentes. Sem declaração de conferência visual autenticada.
### 29/10/2026 — ACT-20261029-01, primeiro incremento instalado: 23ecccfa
## 28/10/2026 — formulários fiscais instalados, 332c16eb
Estado corrente de ACT-01: **parcial — primeiro incremento instalado**, substituindo apenas o estado planeado/preparado deste recorte; ACT-02 a 16 não foram implementadas nesta entrega. Instalado por avanço normal de main e pipeline existente em https://teste-investidor.tempo.co.mz/. Entrada: Gestão → Clusters → Editar → Competitividade — cinco forças de Porter. Preparação qualitativa por sector/região, evidências/fontes/períodos/limites e oportunidades, transferência JSON e recuperação confirmada do mesmo cluster. Sem gravação no servidor nem publicação da análise. Persistência, cobertura factual, território canónico e consulta pública continuam pendentes; esta instalação não encerra o critério amplo ACT-01.
A entrega Git autorizada instalou 332c16eb no portal de teste, depois de capacidade, guarda da versão anterior 789fffd0, backup fresco e restauro isolado conferidos. Disponível: Documentos e Formulários → Preencher IVA — Modelo A / Preparar IRPC — Modelo 22. Rotas públicas IVA/IRPC conferidas HTTP200 e com imposto pedido; rótulos dos acessos e preenchimento conferidos nos assets instalados. Estas provas não substituem a conferência visual autenticada, que permanece pendente.
Provas: backup fresco, restauro isolado sem rede das 176 tabelas; pipeline com lifecycle de schema (3.106 asserções), 59 testes Unit/320 asserções e 1.180 testes frontend sem falhas, tipos e compilação; quatro ficheiros do incremento conferidos por SHA-256 no instalado; C132 no destino passou. Comparação após instalação: todas as 176 tabelas de domínio preservadas, diferenças operacionais apenas em cache/sessões; 369 ficheiros/configuração idênticos. Acesso anónimo ao catálogo privado recusado HTTP401. Zero migrações/importações, nenhuma nova divulgação nem mudança de permissões; versão anterior 332c16eb preservada. Conferência visual autenticada permanece pendente. Recibo: `docs/investmoz/unification/competitiveness-sequential-qa-20261029.json`, em docs/investmoz/unification. As provas aceites anteriores foram reutilizadas; não foram reabertas por esta entrega.
### 29/10/2026 — ACT-01, persistência e aprovação: execução em curso
Ordem confirmada pela Tempo: 1) gravar/validar/recuperar na gestão com permissões e histórico, 2) consultar publicamente apenas conteúdo aprovado, 3) restantes alterações. A instalação da interface 23ecccfa e os 1.180 testes anteriores são provas desse incremento, não prova de persistência. Sem perda nova confirmada; preservar a versão pública anterior e os recibos de backup/restauro. O problema dos filtros provinciais permanece **separado e aberto**, sem declaração de correcção por estes trabalhos.
Escolha técnica: reutilizar snapshots cifrados nos eventos append-only de auditoria, já usados pelo portal; bloqueio transaccional do cluster, controlo de versão e histórico imutável. Sem alteração de schema/permissões. Guardar rascunho e recuperar histórico não publica; aprovação explícita fixa a versão consultável, edições seguintes preservam a aprovação anterior até nova decisão. Rascunhos parciais são guardáveis; aprovação exige os fundamentos das cinco forças. Recuperação/conflito não deve sobrescrever edições inadvertidamente. Acesso público continua limitado às fichas publicadas. Implementação e provas específicas de persistência/consulta ainda em preparação; restantes ACT mantêm o estado anterior.
### ACT-01 — prova específica antes da instalação, 29/10/2026
Sete testes Feature/93 asserções passaram na base restaurada competitiveness_persistence_20261029_test, porta 55441: gravação e leitura HTTP entre pedidos, retorno pela edição da gestão, histórico cifrado, validação de campos/cluster/forças, normalização do formato recuperável, conflitos sem sobrescrita, aprovação fixa, rascunhos posteriores sem divulgação, recuperação histórica como nova versão, retirada e exclusão do conteúdo não aprovado na consulta pública. Utilizadores sem permissão e pedidos anónimos foram recusados na gestão. O teste público percorreu a ficha publicada e confirmou 404 da ficha não publicada. Zero escritas na base activa. Esta prova não afirma conferência visual autenticada nem correcção dos filtros provinciais, que permanecem abertos. Recibo: docs/investmoz/unification/competitiveness-persistence-qa-20261029.json. Compilação QA, tipos e quatro testes frontend dirigidos passaram; instalação ainda pendente.
### 29/10/2026 — ACT-01, gravação/recuperação e consulta aprovada instaladas: b81f477b
Instalado pelo avanço normal de main e pipeline existente, preservando 23ecccfa para retorno. Gestão → Clusters → Editar → Competitividade agora recupera o rascunho do servidor e permite gravar, consultar a versão actual, confirmar recuperação e recuperar uma versão histórica como nova gravação. Versões cifradas e imutáveis na auditoria existente, com bloqueio transaccional do cluster e controlo de versão; conflitos conservam a versão guardada e os campos do editor. Reutilizada a permissão existente de gestão, sem novos papéis ou aprovadores. Rascunhos incompletos podem ser guardados; a aprovação exige âmbito e fundamentos das cinco forças.
**Percurso da gestão:** preencher → Guardar rascunho no servidor → voltar à edição para recuperar → consultar histórico quando necessário. Para comparar com outra gravação, usar Consultar versão actual do servidor e confirmar antes de substituir os campos; transferir primeiro edições que se queira conservar. Recuperar o histórico cria nova versão e mantém o histórico e a aprovação pública existente. A aprovação ou retirada é expressa, com evidência/motivo registado. A ficha pública de um cluster publicado mostra exclusivamente a análise aprovada; novas gravações privadas não alteram essa versão. Uma ficha não publicada continua indisponível ao público. Não houve aprovação nem divulgação automática de análises.
**Prova específica:** sete testes Feature/93 asserções na base restaurada e isolada competitiveness_persistence_20261029_test, porta55441. Abrange gravação/recuperação HTTP, retorno na gestão, validação e formato, histórico cifrado, conflito, acesso recusado, aprovação fixa, rascunho posterior privado, recuperação, retirada e consulta pública/404 de ficha não publicada. Os 1.180 testes frontend não são apresentados como prova substituta de persistência. Pipeline: tipos, compilação, lifecycle de schema1/3106, Unit59/320 e frontend1180 sem falhas. Dez ficheiros de código/testes do instalado conferidos por SHA-256 com os da QA; C132 no destino passou.
**Preservação e limites:** backup fresco/restauro isolado176; comparação posterior de176 tabelas sem alterações de domínio, apenas diferenças operacionais de cache/sessões; 369 ficheiros/configuração idênticos. Ligação real teste_investidor_app conferida em leitura, com privilégios existentes de SELECT/INSERT em auditoria e bloqueio do cluster; endpoint de gestão anónimo HTTP401. Nenhuma escrita sintética na base activa, migração, importação, mudança de permissões ou nova publicação. A conferência visual autenticada no instalado permanece pendente; não foi simulada uma decisão real de conteúdo no activo. ACT-01 tem agora persistência e consulta aprovada implementadas/instaladas, mantendo parcial a cobertura factual por sector/região. Recibo: docs/investmoz/unification/competitiveness-persistence-qa-20261029.json.
**Filtros provinciais: separado e aberto.** Esta entrega não alterou a lógica dos filtros nem demonstra a correcção relatada de Agronegócio/Maputo Província no portal. Preservam-se as provas e os estados históricos; não encerrar V2-07/V2-10 ou ACT-02 por causa da persistência de Competitividade. Restantes alterações permanecem na sequência posterior.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
Cobertura: identificação/período, 27 campos manuais IVA e 24 campos manuais M22, conferência, impressão e ficheiro privado para recuperar mediante confirmação. Sem cálculo, submissão ou assinatura AT; sem gravação no servidor nem associação automática ao dossier. Outros ajustamentos, regimes e anexos M22 não incluídos; L8 permanece parcial. Motores, permissões e provas fiscais anteriores preservados. As 176 tabelas de domínio e os 369 ficheiros/configuração foram comparados; apenas cache/sessões operacionais podem diferir. Base interna preservada, sem publicação: 361.464 observações, não empresas únicas; Gurué 435 observações, cobertura parcial. Relatório instalado: docs/investmoz/unification/fiscal-form-preparation-git-delivery-20261028.json. O relatório QA anterior mantém-se como histórico, não estado corrente de instalação.
@@ -1,177 +0,0 @@
{
"checks": {
"feature_tests": 7,
"isolated_build": "passed",
"feature_assertions": 93,
"frontend_tests": 4,
"types": "passed"
},
"tested_candidate": "568a488ae76c631714547614f52ca6be7a121e2d",
"schema_changes": 0,
"active_writes": 0,
"source_sha256": {
"tests/Feature/ClusterCompetitivenessTest.php": "3c4b27fbc2c00210bd6be78057d88d03f54676c82907fafcbe57843ea7d726cb",
"app/Http/Controllers/InvestmentPortfolioController.php": "0793d06bf94b679b301f4aedbd54d73184ed7b27d3100036dfc81f4621c101d6",
"resources/js/pages/portfolio/show.tsx": "6562798defc1546466b1ed3c66e0d51b1c6c1b08f03c8d5acc915877792ce7da",
"routes/web.php": "76501452ad4ce187ce234127ef160a5a458f629d583d9ccb3c67c1c9d4a1a561",
"resources/js/components/cluster-competitiveness-public.tsx": "85ab2f59de4c63faf6ea699855bbcf0c9efaea0ff9c39421d5640286e9df5910",
"app/Http/Controllers/ClusterCompetitivenessController.php": "12ab13860da1acda02dd77f38d0ddd0d1b8fca5856f7fa8823e9e6193143368e",
"app/Services/ClusterCompetitiveness.php": "24cae918c14a84470b7fc2e1485eacd9c4e2306a54928ca9de6a56576f5227df",
"resources/js/pages/structures/editor.tsx": "c6d50445de3c1f18b266afe5898cd31198fc5946606770ac74a5f01e6e67b712",
"resources/js/pages/structures/competitiveness.tsx": "8c4253cef8450b47245a918712bb81fe0ff2a333f54726a38239264a398f4fde",
"app/Http/Controllers/InvestmentStructureController.php": "b0d11991240fa5f6fa0d56a4c66b63f68665d0687b3772b4f8ca632460499930"
},
"baseline": "23ecccfaad1053f55bf0591fd242cb5fa3406c38",
"provincial_filters": "separate_and_open_not_changed_or_verified",
"remaining": [
"authenticated visual conference of the installed management workflow",
"factual sector and regional analysis and explicit content approvals",
"provincial filter issue remains separate and open",
"remaining planned ACT activities"
],
"status": "installed_specific_persistence_QA_and_preservation_verified_visual_pending",
"proof": {
"candidate": "568a488ae76c631714547614f52ca6be7a121e2d",
"status": "passed",
"database": "competitiveness_persistence_20261029_test",
"port": 55441,
"snapshot_restored": true,
"active_database_writes": 0,
"schema_changes": 0,
"log": "/var/lib/investmoz-deploy/competitiveness-persistence-qa-20261029/qa-568a488ae76c631714547614f52ca6be7a121e2d.log",
"summary": "PHPUnit 12.5.34 by Sebastian Bergmann and contributors.\n\nRuntime: PHP 8.4.27\nConfiguration: /var/lib/investmoz-deploy/competitiveness-persistence-qa-20261029/568a488ae76c631714547614f52ca6be7a121e2d-1793273608890561258/phpunit.xml\n\n....... 7 / 7 (100%)\n\nTime: 00:04.973, Memory: 58.50 MB\n\n\u001b[30;42mOK (7 tests, 93 assertions)\u001b[0m\n"
},
"installed_candidate": "b81f477bf2385227ed3099868e80b1e3c7bcb46b",
"backup": {
"stage": "/var/backups/investmoz-competitiveness-persistence-20261029/backup",
"created_at": "2026-10-29T11:32:49Z",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 176,
"files_verified": 369,
"dump_sha256": "e92123a3b986781de20367a04462ed7b92958c47f232326da18506a79a43717c",
"files_sha256": "bb615875347f7b27da593f148af973fc825555524a261f8f68c50cd4ce26962c",
"restore_verified": false
},
"restore": {
"restore_verified": true,
"tables_verified": 176,
"container_network": "none",
"database": "competitiveness_persistence_restore_20261029_test",
"port": 55441,
"source_dump_sha256": "e92123a3b986781de20367a04462ed7b92958c47f232326da18506a79a43717c",
"active_database_writes": 0
},
"code": {
"candidate": "b81f477bf2385227ed3099868e80b1e3c7bcb46b",
"source_files_verified": 10,
"diagnostics_C132": {
"result": "passed",
"case": "C132",
"source_explicit_constant_OFF": true,
"effective_configuration_OFF": true,
"configuration_cached": false,
"database_connections_used": 0,
"active_writes": 0
},
"active_writes": 0
},
"runtime": {
"identity": {
"database": "teste_investidor",
"role": "teste_investidor_app",
"port": 5432,
"audit_read": true,
"audit_append": true,
"cluster_lock": true
},
"management_read_from_database": true,
"server_version": 0,
"history_items": 0,
"approved_projection_available": false,
"actor_impersonation": false,
"active_domain_writes": 0,
"candidate": "b81f477bf2385227ed3099868e80b1e3c7bcb46b",
"anonymous_management_status": 401,
"persistence_actions_compiled": true,
"specific_write_recovery_and_public_projection_QA": "7 tests / 93 assertions; no synthetic writes on active database",
"provincial_filters": "separate and open; not changed or confirmed here",
"authenticated_visual_conference": "pending"
},
"preservation": {
"navigation_asset_and_shared_props_verified": true,
"at": "2026-10-29T11:42:15.258836+00:00",
"served_commit": "b81f477bf2385227ed3099868e80b1e3c7bcb46b",
"backup_dump_sha256": "e92123a3b986781de20367a04462ed7b92958c47f232326da18506a79a43717c",
"prior_public_tables_checked": 176,
"prior_domain_rows_preserved": true,
"new_migration_registry_rows": 0,
"operational_delta": {
"cache": {
"before": {
"rows": 119,
"checksum": "5ddea5ecf35f8dc04d275b189821b59a"
},
"after": {
"rows": 119,
"checksum": "8336a458d425e14c66b20c3da9aa01de"
}
},
"sessions": {
"before": {
"rows": 10,
"checksum": "aa65128c0bafc9eab8419174754327d7"
},
"after": {
"rows": 12,
"checksum": "41f44e385b8a31b0af1f22e1a29680ce"
}
}
},
"persistent_files_and_configuration_identical": 369,
"private_catalogue": {
"observations": 361464,
"distinct_administrative_observations": 214299,
"preserved_repetitions": 147165,
"unmapped_territory": 205608
},
"preservation_identity": "postgres, matching backup reader; no actor impersonation",
"anonymous_query_status": 401,
"public_revision_confirmed": true,
"actual_team_UI_journey": "pending authenticated browser conference;accepted QA is not repeated",
"unique_company_count": null,
"publication": false,
"national_coverage": "partial",
"investment_conclusion": "hypothesis,not proved demand or viability",
"L8": "unchanged,partial"
},
"pipeline": {
"unit_tests": 59,
"summaries": [
"\u001b[30;42mOK (1 test, 3106 assertions)\u001b[0m",
"✓ built in 33.96s",
"\u001b[30;42mOK (59 tests, 320 assertions)\u001b[0m",
"# tests 1180",
"# pass 1180",
"# fail 0"
],
"schema_assertions": 3106,
"frontend_tests": 1180,
"result": {
"status": "installed",
"commit": "b81f477bf2385227ed3099868e80b1e3c7bcb46b",
"release": "/var/www/investmoz-homologacao/releases/b81f477bf2385227ed3099868e80b1e3c7bcb46b-5eed771e",
"previous_release": "/var/www/investmoz-homologacao/releases/23ecccfaad1053f55bf0591fd242cb5fa3406c38-0e0b8a65",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"b81f477bf2385227ed3099868e80b1e3c7bcb46b"
],
"finished_at": "2026-10-29T11:39:35.927983+00:00"
},
"frontend_failures": 0,
"unit_assertions": 320
}
}
@@ -1,147 +0,0 @@
{
"activity": "ACT-20261029-01",
"status": "partial_increment_installed_preservation_verified_visual_pending",
"baseline": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"branch": "codex/competitiveness-sequential-20261029",
"target": "https://teste-investidor.tempo.co.mz/",
"checks": {
"diff": "passed",
"types": "passed",
"diagnostics_C132": "passed_in_original_workspace",
"frontend_tests": 4,
"isolated_qa_build": "passed"
},
"active_database_writes": 0,
"schema_changes": false,
"permission_changes": false,
"coverage": [
"five qualitative forces in existing cluster management",
"sector and regional scope",
"evidence, source, period, rationale, limitations and investment opportunity",
"explicit uncertainty",
"JSON download and confirmed same-cluster recovery"
],
"remaining": [
"authenticated visual verification",
"server persistence and canonical territorial scope",
"factual sector and regional data",
"public consultation"
],
"source_sha256": {
"resources/js/lib/cluster-competitiveness.ts": "1ed496f51f020da9f0c02ab0aa324b58a9dd17fa34a10d8c1a90d9915800fecb",
"resources/js/pages/structures/competitiveness.tsx": "55338c144cefae4f91cef765b8e5e8b28ceb6c76375d46943c3c3430aeddc68e",
"resources/js/pages/structures/editor.tsx": "080a90ab8607f05ec493e560ed8280a6f7eec743ed1954f0057a0ce6caa09208",
"tests/frontend/cluster-competitiveness.test.mjs": "f48a9ed872f49ad900424daab4689cb0eeeb31cc93ebf1c608408c0a8c2b435b"
},
"candidate": "23ecccfaad1053f55bf0591fd242cb5fa3406c38",
"backup": {
"stage": "/var/backups/investmoz-competitiveness-delivery-20261029/backup",
"created_at": "2026-10-29T11:04:46Z",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 176,
"files_verified": 369,
"dump_sha256": "7979fa2b3fe1f69e62aad9fadf60a60d7b068e198cdc291a8fc79d409d67c7ca",
"files_sha256": "8e0022b73cd0074e36f8efef0b7c803a386e24e6e4cf85f567757ff67e311369",
"restore_verified": false
},
"restore": {
"restore_verified": true,
"tables_verified": 176,
"container_network": "none",
"database": "competitiveness_delivery_20261029_test",
"port": 55441,
"source_dump_sha256": "7979fa2b3fe1f69e62aad9fadf60a60d7b068e198cdc291a8fc79d409d67c7ca",
"active_database_writes": 0
},
"preservation": {
"navigation_asset_and_shared_props_verified": true,
"at": "2026-10-29T11:10:44.303271+00:00",
"served_commit": "23ecccfaad1053f55bf0591fd242cb5fa3406c38",
"backup_dump_sha256": "7979fa2b3fe1f69e62aad9fadf60a60d7b068e198cdc291a8fc79d409d67c7ca",
"prior_public_tables_checked": 176,
"prior_domain_rows_preserved": true,
"new_migration_registry_rows": 0,
"operational_delta": {
"cache": {
"before": {
"rows": 119,
"checksum": "5f3c36637fbf1e9dc4700066d4c45530"
},
"after": {
"rows": 119,
"checksum": "b1c9949cfacc34906c406e27e95cbd9b"
}
},
"sessions": {
"before": {
"rows": 37,
"checksum": "b0e409c4c77c58b48252d9847c3c61ef"
},
"after": {
"rows": 38,
"checksum": "f4d94db55488bc0bcf6f727f222473c1"
}
}
},
"persistent_files_and_configuration_identical": 369,
"private_catalogue": {
"observations": 361464,
"distinct_administrative_observations": 214299,
"preserved_repetitions": 147165,
"unmapped_territory": 205608
},
"preservation_identity": "postgres, matching backup reader; no actor impersonation",
"anonymous_query_status": 401,
"public_revision_confirmed": true,
"actual_team_UI_journey": "pending authenticated browser conference;accepted QA is not repeated",
"unique_company_count": null,
"publication": false,
"national_coverage": "partial",
"investment_conclusion": "hypothesis,not proved demand or viability",
"L8": "unchanged,partial"
},
"installed_code": {
"candidate": "23ecccfaad1053f55bf0591fd242cb5fa3406c38",
"source_files_verified": 4,
"diagnostics_C132": {
"result": "passed",
"case": "C132",
"source_explicit_constant_OFF": true,
"effective_configuration_OFF": true,
"configuration_cached": false,
"database_connections_used": 0,
"active_writes": 0
},
"active_writes": 0
},
"pipeline": {
"unit_tests": 59,
"frontend_failures": 0,
"schema_assertions": 3106,
"checks": [
"\u001b[30;42mOK (1 test, 3106 assertions)\u001b[0m",
"✓ built in 34.50s",
"\u001b[30;42mOK (59 tests, 320 assertions)\u001b[0m",
"# tests 1180",
"# pass 1180",
"# fail 0"
],
"frontend_tests": 1180,
"result": {
"status": "installed",
"commit": "23ecccfaad1053f55bf0591fd242cb5fa3406c38",
"release": "/var/www/investmoz-homologacao/releases/23ecccfaad1053f55bf0591fd242cb5fa3406c38-0e0b8a65",
"previous_release": "/var/www/investmoz-homologacao/releases/332c16ebb555e8d6b244fa519bc777d89c1a8b8f-f0dfc218",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"23ecccfaad1053f55bf0591fd242cb5fa3406c38"
],
"finished_at": "2026-10-29T11:10:02.884033+00:00"
},
"unit_assertions": 320
}
}
@@ -0,0 +1,213 @@
{
"status": "installed_delta_verified_authenticated_visual_conference_pending",
"candidate": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"expected_installed": "789fffd098fa222c85e1283de07f23ee6f42bd04",
"target": "https://teste-investidor.tempo.co.mz/",
"coverage": {
"IVA": "27 manually editable Modelo A fields; source correspondence reused",
"IRPC": "24 manually editable M22 fields from Q10/Q11 general regime/Q12; partial preparation",
"flow": [
"identification and period",
"amounts",
"review",
"download private recovery file",
"explicit recovery confirmation",
"print"
],
"entry": "Documentos e Formulários → IVA or IRPC"
},
"limits": [
"No automatic calculations or eligibility validation",
"No AT submission or signature",
"No server persistence or automatic dossier link",
"Remaining M22 adjustments, other regimes and annexes not covered",
"Manual credit entries require parcel conference; existing calculation engines unchanged",
"L8 remains partial",
"Authenticated visual browser validation not demonstrated"
],
"preserved": [
"installed 789fffd0 features",
"accepted Modelo A and M20/M22 proofs",
"L8 accepted increments",
"all originals, data, roles and internal supply base; no active writes"
],
"QA": {
"frontend_tests": 5,
"feature_tests": 1,
"feature_assertions": 20,
"isolated_database": "proactive_investment_20261027_test",
"port": 55441,
"types": "passed",
"build": "passed",
"PHP_syntax": "passed",
"diff_check": "passed",
"source_visual_check": "M22 three existing page renders inspected; accepted IVA mapping reused"
},
"schema_changes": false,
"role_changes": false,
"new_publications": false,
"active_changes": "332c16eb installed through normal main Git pipeline",
"main_ancestor_verified": true,
"delivery_next_steps": [
"Authenticated visual conference of filling, review, download and recovery"
],
"source_code_sha256": {
"resources\\js\\pages\\portal\\fiscal-forms.tsx": "d27d16b174f6a9784b2ad0a2f6e46563d75192ebe4064d700b0df59e953317e0",
"resources\\js\\lib\\fiscal-form-preparation.ts": "776a563b4ebc1ddc6ceb14e2e8b1e6c9542e8cc9c17053811f6381d83bf8b3ec",
"routes\\web.php": "a25b4dccbf64b123117573ea31b83cfcc1ee642db9b0f95ed2f77387696879d9"
},
"backup": {
"stage": "/var/backups/investmoz-fiscal-forms-delivery-20261028/backup",
"created_at": "2026-10-28T20:07:20Z",
"target": {
"database": "teste_investidor",
"port": 5432
},
"tables": 176,
"files_verified": 369,
"dump_sha256": "341662f3c8f6e6c7b42d18aa468f3270d9d6a08bb908b7f52cbb1e132e5bd976",
"files_sha256": "c6b242850118003792b420f1e2706633489dc874bef664b2245dfbd5dbb50430",
"restore_verified": false
},
"restore": {
"restore_verified": true,
"tables_verified": 176,
"container_network": "none",
"database": "fiscal_forms_delivery_20261028_test",
"port": 55441,
"source_dump_sha256": "341662f3c8f6e6c7b42d18aa468f3270d9d6a08bb908b7f52cbb1e132e5bd976",
"active_database_writes": 0
},
"preservation": {
"navigation_asset_and_shared_props_verified": true,
"at": "2026-10-28T20:13:13.258269+00:00",
"served_commit": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"backup_dump_sha256": "341662f3c8f6e6c7b42d18aa468f3270d9d6a08bb908b7f52cbb1e132e5bd976",
"prior_public_tables_checked": 176,
"prior_domain_rows_preserved": true,
"new_migration_registry_rows": 0,
"operational_delta": {
"cache": {
"before": {
"rows": 119,
"checksum": "c98548e1038986975b95a959d469b828"
},
"after": {
"rows": 119,
"checksum": "5375b8b28b8af40c8c3ffc891975d6d5"
}
},
"sessions": {
"before": {
"rows": 25,
"checksum": "bcf0421fabf8473d5862941ec18e96af"
},
"after": {
"rows": 28,
"checksum": "78066be68c90956a3ac31a90bbeba676"
}
}
},
"persistent_files_and_configuration_identical": 369,
"private_catalogue": {
"observations": 361464,
"distinct_administrative_observations": 214299,
"preserved_repetitions": 147165,
"unmapped_territory": 205608
},
"preservation_identity": "postgres, matching backup reader; no actor impersonation",
"anonymous_query_status": 401,
"public_revision_confirmed": true,
"actual_team_UI_journey": "pending authenticated browser conference;accepted QA is not repeated",
"unique_company_count": null,
"publication": false,
"national_coverage": "partial",
"investment_conclusion": "hypothesis,not proved demand or viability",
"L8": "unchanged,partial"
},
"public_delta": {
"at_UTC": "2026-10-28T20:13:02.411009+00:00",
"candidate": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"public_pages": [
{
"tax": "iva",
"HTTP": 200,
"component": "portal/fiscal-forms",
"requested_form": "iva"
},
{
"tax": "irpc",
"HTTP": 200,
"component": "portal/fiscal-forms",
"requested_form": "irpc"
}
],
"assets": {
"fiscal-forms": {
"asset": "assets/fiscal-forms-CQflZEeP.js",
"sha256": "f559122ea47ac15d061ed4b2e008beeb0e9815626afd50315bd465ea6109bde0",
"expected_labels": 4
},
"services": {
"asset": "assets/services-DOFOHuLj.js",
"sha256": "4a9de7c6dd6d1d48adf55e47b6dd8561af2a53163f63eb14b0f8d9d135fd66d7",
"expected_labels": 2
}
},
"authenticated_visual_conference": "pending; connector availability not established",
"AT_submission": false,
"server_draft_persistence": false,
"L8": "partial"
},
"pipeline": {
"reports": [
{
"status": "already_installed",
"commit": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"requested_commits": [
"332c16ebb555e8d6b244fa519bc777d89c1a8b8f"
],
"finished_at": "2026-10-28T20:12:04.008171+00:00"
},
{
"status": "installed",
"commit": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"release": "/var/www/investmoz-homologacao/releases/332c16ebb555e8d6b244fa519bc777d89c1a8b8f-f0dfc218",
"previous_release": "/var/www/investmoz-homologacao/releases/789fffd098fa222c85e1283de07f23ee6f42bd04-29ab1f0d",
"database_migrations": 0,
"content_imports": 0,
"requested_commits": [
"332c16ebb555e8d6b244fa519bc777d89c1a8b8f"
],
"finished_at": "2026-10-28T20:12:02.675406+00:00"
}
],
"build_logs": [
{
"file": "/var/lib/investmoz-deploy/logs/1793218102227589181.log",
"sha256": "670358412945027ec26a2c7e2129a06b0af164aac26990db5d73c9aeb5ba9b6c",
"summaries": [
"\u001b[30;42mOK (1 test, 3106 assertions)\u001b[0m",
"✓ built in 32.91s",
"\u001b[30;42mOK (59 tests, 320 assertions)\u001b[0m",
"# tests 1176",
"# pass 1176",
"# fail 0"
]
}
]
},
"delivery_completed": [
"SSH authentication",
"capacity and baseline guard",
"fresh backup",
"isolated restore",
"normal Git main delivery",
"existing pipeline: 1176 frontend tests passed",
"public route/assets delta",
"176 table preservation comparison and 369 identical files/configuration"
],
"verification_notes": [
"Initial X-Inertia probe without matching asset version received protocol HTTP409. Normal initial page requests returned200; initial public page payload and installed assets verified. Failed probe receipt retained; no application change needed."
]
}
@@ -0,0 +1,63 @@
{
"status": "QA_passed_not_installed_SSH_authentication_required",
"candidate": "332c16ebb555e8d6b244fa519bc777d89c1a8b8f",
"expected_installed": "789fffd098fa222c85e1283de07f23ee6f42bd04",
"target": "https://teste-investidor.tempo.co.mz/",
"coverage": {
"IVA": "27 manually editable Modelo A fields; source correspondence reused",
"IRPC": "24 manually editable M22 fields from Q10/Q11 general regime/Q12; partial preparation",
"flow": [
"identification and period",
"amounts",
"review",
"download private recovery file",
"explicit recovery confirmation",
"print"
],
"entry": "Documentos e Formulários → IVA or IRPC"
},
"limits": [
"No automatic calculations or eligibility validation",
"No AT submission or signature",
"No server persistence or automatic dossier link",
"Remaining M22 adjustments, other regimes and annexes not covered",
"Manual credit entries require parcel conference; existing calculation engines unchanged",
"L8 remains partial",
"Authenticated visual browser validation not demonstrated"
],
"preserved": [
"installed 789fffd0 features",
"accepted Modelo A and M20/M22 proofs",
"L8 accepted increments",
"all originals, data, roles and internal supply base; no active writes"
],
"QA": {
"frontend_tests": 5,
"feature_tests": 1,
"feature_assertions": 20,
"isolated_database": "proactive_investment_20261027_test",
"port": 55441,
"types": "passed",
"build": "passed",
"PHP_syntax": "passed",
"diff_check": "passed",
"source_visual_check": "M22 three existing page renders inspected; accepted IVA mapping reused"
},
"schema_changes": false,
"role_changes": false,
"new_publications": false,
"active_changes": false,
"main_ancestor_verified": true,
"delivery_next_steps": [
"authenticate SSH using existing strict host-key bridge",
"recheck installed baseline, capacity and schema contract",
"fresh backup and isolated restoration",
"normal Git main delivery and existing guarded pipeline",
"installed route/assets verification and full preservation delta"
],
"source_code_sha256": {
"resources\\js\\pages\\portal\\fiscal-forms.tsx": "d27d16b174f6a9784b2ad0a2f6e46563d75192ebe4064d700b0df59e953317e0",
"resources\\js\\lib\\fiscal-form-preparation.ts": "776a563b4ebc1ddc6ceb14e2e8b1e6c9542e8cc9c17053811f6381d83bf8b3ec",
"routes\\web.php": "a25b4dccbf64b123117573ea31b83cfcc1ee642db9b0f95ed2f77387696879d9"
}
}
@@ -7386,39 +7386,15 @@ Pedido expresso da Tempo: preencher formulário de IVA ou IRPC, em vez de abrir
Incremento em preparação QA: entrada directa em Documentos e Formulários, preenchimento manual dos campos correspondidos de IVA e subconjunto M22, identificação/período, revisão, impressão e ficheiro privado para recuperação mediante confirmação. Escolha técnica: sem persistência no servidor nem ligação automática ao dossier neste incremento; não altera schema, dados, permissões ou cálculos. Ficheiro não publicado. IRPC ajustamentos completos, outros regimes, anexos, assinatura e submissão à AT fora da cobertura; L8 permanece parcial. Ainda sem instalação deste incremento. Próximas provas apenas do delta: fidelidade da recuperação, rejeição de ficheiros incompatíveis sem perda, formatos/valores desconhecidos, acesso público e compilação.
## 29/10/2026 — ACT-20261029-01, inventário antes da verificação do incremento
## 28/10/2026 — formulários fiscais 332c16eb, QA concluída e autenticação pendente
Continuação sequencial solicitada pela Tempo. Correspondência: ACT-20261029-01 reutiliza D-02 §8, clusters/cadeias e a orientação metodológica documentada em 23/10. Concluído previamente e preservado: gestão de clusters, etapas, ligações, lacunas e relações auditadas; disponibilidade instalada de carteira/âncoras e formulários fiscais conforme recibos de 28/10. Nenhuma perda nova confirmada nesta inspecção de código; não foi feita nova comparação de dados ou alegada prova do servidor. A pasta principal está em fde644af, enquanto o último recibo identifica 332c16eb instalado; reconciliar o delta sobre essa origem antes de instalar, sem substituir o destino pela pasta principal.
Documentos e Formulários disponibiliza no candidato entradas directas IVA/IRPC: identificação, período, 27 campos manuais de Modelo A ou 24 campos manuais de M22, revisão, impressão, ficheiro privado e recuperação com confirmação antes de substituir. Sem geração de zeros, arredondamento, apuramento ou submissão à AT. IRPC permanece parcial: outros ajustamentos, regimes e anexos não incluídos; L8 parcial. Persistência no servidor e associação automática ao dossier não incluídas. Cinco testes frontend e um teste Feature/20 asserções passaram na base QA isolada 127.0.0.1:55441/proactive_investment_20261027_test; tipos, compilação, sintaxe PHP e diff conferidos. Não equivalem a validação visual autenticada. Relatório: docs/investmoz/unification/fiscal-form-preparation-qa-20261028.json.
Incremento em preparação: diagnóstico qualitativo das cinco forças na edição do cluster, sector/região editáveis, fonte/período/evidência/justificação/limites/oportunidade por força; indeterminação explícita, ficheiro privado transferível e recuperação com confirmação. Sem alteração de schema, dados, permissões, publicações ou motores fiscais. Persistência no servidor, ligação territorial canónica, dados sectoriais fundamentados e consulta pública permanecem por implementar; ACT-01 não está concluída. Verificação dirigida e instalação ainda pendentes neste registo inicial. Restantes ACT-02 a 16 mantêm o estado anterior e a sequência, sem trabalho SGF nesta passagem.
### ACT-20261029-01 — candidato sequencial preparado, 29/10/2026
Candidato publicado apenas no ramo Git codex/fiscal-forms-qa-20261028; main e activo não alterados. Instalação anterior 789fffd0 preservada. Preparação de entrega em outputs/fiscal-forms-delivery-20261028, com guarda do destino, backup fresco, restauro isolado e pipeline existentes; execução remota depende de nova autenticação SSH, pois a sessão anterior foi encerrada.
Primeiro incremento implementado, **parcial e ainda não instalado**: gestão → clusters → editar → Competitividade. As cinco forças conservam fonte, período, evidência, apreciação, limites e oportunidade; sector e região explícitos, sem inferências automáticas. Transferência de rascunhos JSON e recuperação restrita ao mesmo cluster, com confirmação antes de substituir. Quatro testes frontend dirigidos, TypeScript e compilação QA passaram tanto na pasta de trabalho como no candidato baseado em 332c16eb. Guarda C132 local passou; não foram efectuados testes nem escritas na base activa. A edição existente recebe apenas a inclusão do componente; contratos e permissões existentes preservados.
Origem reconciliada: main remoto e revisão pública conferidos em 332c16eb em 29/10. Candidato em C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-sequential-20261029. Consulta do estado remoto não equivale a inventário novo da base. Sessão SSH anterior encerrada; autenticação por chave recusada. Instalação requer autenticação SSH, backup fresco, restauro isolado, guarda de versão, pipeline existente e conferência posterior. Não instalar a pasta principal sobre o destino. A sequência solicitada mantém ACT-02 a 16 por iniciar após a instalação e o saldo de ACT-01; persistência no servidor, cobertura factual e consulta pública continuam pendentes. Sem declaração de conferência visual autenticada.
### 29/10/2026 — ACT-20261029-01, primeiro incremento instalado: 23ecccfa
## 28/10/2026 — formulários fiscais instalados, 332c16eb
Estado corrente de ACT-01: **parcial — primeiro incremento instalado**, substituindo apenas o estado planeado/preparado deste recorte; ACT-02 a 16 não foram implementadas nesta entrega. Instalado por avanço normal de main e pipeline existente em https://teste-investidor.tempo.co.mz/. Entrada: Gestão → Clusters → Editar → Competitividade — cinco forças de Porter. Preparação qualitativa por sector/região, evidências/fontes/períodos/limites e oportunidades, transferência JSON e recuperação confirmada do mesmo cluster. Sem gravação no servidor nem publicação da análise. Persistência, cobertura factual, território canónico e consulta pública continuam pendentes; esta instalação não encerra o critério amplo ACT-01.
A entrega Git autorizada instalou 332c16eb no portal de teste, depois de capacidade, guarda da versão anterior 789fffd0, backup fresco e restauro isolado conferidos. Disponível: Documentos e Formulários → Preencher IVA — Modelo A / Preparar IRPC — Modelo 22. Rotas públicas IVA/IRPC conferidas HTTP200 e com imposto pedido; rótulos dos acessos e preenchimento conferidos nos assets instalados. Estas provas não substituem a conferência visual autenticada, que permanece pendente.
Provas: backup fresco, restauro isolado sem rede das 176 tabelas; pipeline com lifecycle de schema (3.106 asserções), 59 testes Unit/320 asserções e 1.180 testes frontend sem falhas, tipos e compilação; quatro ficheiros do incremento conferidos por SHA-256 no instalado; C132 no destino passou. Comparação após instalação: todas as 176 tabelas de domínio preservadas, diferenças operacionais apenas em cache/sessões; 369 ficheiros/configuração idênticos. Acesso anónimo ao catálogo privado recusado HTTP401. Zero migrações/importações, nenhuma nova divulgação nem mudança de permissões; versão anterior 332c16eb preservada. Conferência visual autenticada permanece pendente. Recibo: `docs/investmoz/unification/competitiveness-sequential-qa-20261029.json`, em docs/investmoz/unification. As provas aceites anteriores foram reutilizadas; não foram reabertas por esta entrega.
### 29/10/2026 — ACT-01, persistência e aprovação: execução em curso
Ordem confirmada pela Tempo: 1) gravar/validar/recuperar na gestão com permissões e histórico, 2) consultar publicamente apenas conteúdo aprovado, 3) restantes alterações. A instalação da interface 23ecccfa e os 1.180 testes anteriores são provas desse incremento, não prova de persistência. Sem perda nova confirmada; preservar a versão pública anterior e os recibos de backup/restauro. O problema dos filtros provinciais permanece **separado e aberto**, sem declaração de correcção por estes trabalhos.
Escolha técnica: reutilizar snapshots cifrados nos eventos append-only de auditoria, já usados pelo portal; bloqueio transaccional do cluster, controlo de versão e histórico imutável. Sem alteração de schema/permissões. Guardar rascunho e recuperar histórico não publica; aprovação explícita fixa a versão consultável, edições seguintes preservam a aprovação anterior até nova decisão. Rascunhos parciais são guardáveis; aprovação exige os fundamentos das cinco forças. Recuperação/conflito não deve sobrescrever edições inadvertidamente. Acesso público continua limitado às fichas publicadas. Implementação e provas específicas de persistência/consulta ainda em preparação; restantes ACT mantêm o estado anterior.
### ACT-01 — prova específica antes da instalação, 29/10/2026
Sete testes Feature/93 asserções passaram na base restaurada competitiveness_persistence_20261029_test, porta 55441: gravação e leitura HTTP entre pedidos, retorno pela edição da gestão, histórico cifrado, validação de campos/cluster/forças, normalização do formato recuperável, conflitos sem sobrescrita, aprovação fixa, rascunhos posteriores sem divulgação, recuperação histórica como nova versão, retirada e exclusão do conteúdo não aprovado na consulta pública. Utilizadores sem permissão e pedidos anónimos foram recusados na gestão. O teste público percorreu a ficha publicada e confirmou 404 da ficha não publicada. Zero escritas na base activa. Esta prova não afirma conferência visual autenticada nem correcção dos filtros provinciais, que permanecem abertos. Recibo: docs/investmoz/unification/competitiveness-persistence-qa-20261029.json. Compilação QA, tipos e quatro testes frontend dirigidos passaram; instalação ainda pendente.
### 29/10/2026 — ACT-01, gravação/recuperação e consulta aprovada instaladas: b81f477b
Instalado pelo avanço normal de main e pipeline existente, preservando 23ecccfa para retorno. Gestão → Clusters → Editar → Competitividade agora recupera o rascunho do servidor e permite gravar, consultar a versão actual, confirmar recuperação e recuperar uma versão histórica como nova gravação. Versões cifradas e imutáveis na auditoria existente, com bloqueio transaccional do cluster e controlo de versão; conflitos conservam a versão guardada e os campos do editor. Reutilizada a permissão existente de gestão, sem novos papéis ou aprovadores. Rascunhos incompletos podem ser guardados; a aprovação exige âmbito e fundamentos das cinco forças.
**Percurso da gestão:** preencher → Guardar rascunho no servidor → voltar à edição para recuperar → consultar histórico quando necessário. Para comparar com outra gravação, usar Consultar versão actual do servidor e confirmar antes de substituir os campos; transferir primeiro edições que se queira conservar. Recuperar o histórico cria nova versão e mantém o histórico e a aprovação pública existente. A aprovação ou retirada é expressa, com evidência/motivo registado. A ficha pública de um cluster publicado mostra exclusivamente a análise aprovada; novas gravações privadas não alteram essa versão. Uma ficha não publicada continua indisponível ao público. Não houve aprovação nem divulgação automática de análises.
**Prova específica:** sete testes Feature/93 asserções na base restaurada e isolada competitiveness_persistence_20261029_test, porta55441. Abrange gravação/recuperação HTTP, retorno na gestão, validação e formato, histórico cifrado, conflito, acesso recusado, aprovação fixa, rascunho posterior privado, recuperação, retirada e consulta pública/404 de ficha não publicada. Os 1.180 testes frontend não são apresentados como prova substituta de persistência. Pipeline: tipos, compilação, lifecycle de schema1/3106, Unit59/320 e frontend1180 sem falhas. Dez ficheiros de código/testes do instalado conferidos por SHA-256 com os da QA; C132 no destino passou.
**Preservação e limites:** backup fresco/restauro isolado176; comparação posterior de176 tabelas sem alterações de domínio, apenas diferenças operacionais de cache/sessões; 369 ficheiros/configuração idênticos. Ligação real teste_investidor_app conferida em leitura, com privilégios existentes de SELECT/INSERT em auditoria e bloqueio do cluster; endpoint de gestão anónimo HTTP401. Nenhuma escrita sintética na base activa, migração, importação, mudança de permissões ou nova publicação. A conferência visual autenticada no instalado permanece pendente; não foi simulada uma decisão real de conteúdo no activo. ACT-01 tem agora persistência e consulta aprovada implementadas/instaladas, mantendo parcial a cobertura factual por sector/região. Recibo: docs/investmoz/unification/competitiveness-persistence-qa-20261029.json.
**Filtros provinciais: separado e aberto.** Esta entrega não alterou a lógica dos filtros nem demonstra a correcção relatada de Agronegócio/Maputo Província no portal. Preservam-se as provas e os estados históricos; não encerrar V2-07/V2-10 ou ACT-02 por causa da persistência de Competitividade. Restantes alterações permanecem na sequência posterior.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
Cobertura: identificação/período, 27 campos manuais IVA e 24 campos manuais M22, conferência, impressão e ficheiro privado para recuperar mediante confirmação. Sem cálculo, submissão ou assinatura AT; sem gravação no servidor nem associação automática ao dossier. Outros ajustamentos, regimes e anexos M22 não incluídos; L8 permanece parcial. Motores, permissões e provas fiscais anteriores preservados. As 176 tabelas de domínio e os 369 ficheiros/configuração foram comparados; apenas cache/sessões operacionais podem diferir. Base interna preservada, sem publicação: 361.464 observações, não empresas únicas; Gurué 435 observações, cobertura parcial. Relatório instalado: docs/investmoz/unification/fiscal-form-preparation-git-delivery-20261028.json. O relatório QA anterior mantém-se como histórico, não estado corrente de instalação.
@@ -3006,41 +3006,3 @@ AUD-SIM-INT01 through AUD-SIM-INT08 are recorded in the same matrix and manual.
Actualização QA de 20/10/2026: os mesmos oito itens foram reconciliados no manual e nas duas tabelas existentes. Data com mensagem junto ao campo; inventário dos parâmetros efectivos (zero regras fiscais QA); controlos matemáticos independentes; aviso de recálculo; quatro relatórios reais do exportador com 6/6/6/5 páginas; consulta fiscal com erro/nova tentativa e traduções comuns verificados. O formulário personalizado já não exige contexto fiscal que não envia ao cálculo. Restam continuidade autenticada entre dois dossiers/nova sessão e histórico, mudança de parâmetros, controlos delimitados de cash-flow/plano, impressão nativa Edge/comparação pela interface e mensagens dinâmicas/falhas dirigidas de gravação/exportação. Estimativa restante **6–10 h**, distinta das 18–30 h iniciais. Sem main, servidor remoto, importação ou activação fiscal. Exemplos PDF são renderizações do DOCX; não certificam impressão do Edge. Ver o saldo detalhado AUD-SIM-INT01–08 no mesmo manual.
Continuação delimitada: dois dossiers e nova entrada conferidos no navegador; histórico agora visível no percurso de pequeno negócio, hashes preservados; Abrir cálculo encaminha para a etapa financeira; repetição de gravação após 503 corrigida e conferida; controlos independentes de tesouraria/plano passaram. CSV e Word descarregados pela interface coincidem nos valores comuns e apresentam a receita completa do crédito. Seis páginas do DOCX conferidas; PDF nativo ainda aguarda gravação pelo utilizador. QA tem zero regras com visibilidade completa, mas existem três originais fiscais versionados e análises/referências reutilizáveis; não se concluiu ausência na homologação sem autenticação SSH. Versões sintéticas testadas apenas em memória, sem cadastro/activação. Encontrada e corrigida perda dos pressupostos da tesouraria: snapshot no JSON existente, recuperação por dossier e relatório, sem migration; testes HTTP/frontend passaram, falta conferência visual deste novo delta. Saldo **2–4 h** após acessos/ficheiro, detalhado no mesmo manual; integração do motor fiscal permanece separada e não foi implementada. Sem main ou intervenção remota.
## 29/10/2026 — ACT-20261029-01, inventário antes da verificação do incremento
Continuação sequencial solicitada pela Tempo. Correspondência: ACT-20261029-01 reutiliza D-02 §8, clusters/cadeias e a orientação metodológica documentada em 23/10. Concluído previamente e preservado: gestão de clusters, etapas, ligações, lacunas e relações auditadas; disponibilidade instalada de carteira/âncoras e formulários fiscais conforme recibos de 28/10. Nenhuma perda nova confirmada nesta inspecção de código; não foi feita nova comparação de dados ou alegada prova do servidor. A pasta principal está em fde644af, enquanto o último recibo identifica 332c16eb instalado; reconciliar o delta sobre essa origem antes de instalar, sem substituir o destino pela pasta principal.
Incremento em preparação: diagnóstico qualitativo das cinco forças na edição do cluster, sector/região editáveis, fonte/período/evidência/justificação/limites/oportunidade por força; indeterminação explícita, ficheiro privado transferível e recuperação com confirmação. Sem alteração de schema, dados, permissões, publicações ou motores fiscais. Persistência no servidor, ligação territorial canónica, dados sectoriais fundamentados e consulta pública permanecem por implementar; ACT-01 não está concluída. Verificação dirigida e instalação ainda pendentes neste registo inicial. Restantes ACT-02 a 16 mantêm o estado anterior e a sequência, sem trabalho SGF nesta passagem.
### ACT-20261029-01 — candidato sequencial preparado, 29/10/2026
Primeiro incremento implementado, **parcial e ainda não instalado**: gestão → clusters → editar → Competitividade. As cinco forças conservam fonte, período, evidência, apreciação, limites e oportunidade; sector e região explícitos, sem inferências automáticas. Transferência de rascunhos JSON e recuperação restrita ao mesmo cluster, com confirmação antes de substituir. Quatro testes frontend dirigidos, TypeScript e compilação QA passaram tanto na pasta de trabalho como no candidato baseado em 332c16eb. Guarda C132 local passou; não foram efectuados testes nem escritas na base activa. A edição existente recebe apenas a inclusão do componente; contratos e permissões existentes preservados.
Origem reconciliada: main remoto e revisão pública conferidos em 332c16eb em 29/10. Candidato em C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-sequential-20261029. Consulta do estado remoto não equivale a inventário novo da base. Sessão SSH anterior encerrada; autenticação por chave recusada. Instalação requer autenticação SSH, backup fresco, restauro isolado, guarda de versão, pipeline existente e conferência posterior. Não instalar a pasta principal sobre o destino. A sequência solicitada mantém ACT-02 a 16 por iniciar após a instalação e o saldo de ACT-01; persistência no servidor, cobertura factual e consulta pública continuam pendentes. Sem declaração de conferência visual autenticada.
### 29/10/2026 — ACT-20261029-01, primeiro incremento instalado: 23ecccfa
Estado corrente de ACT-01: **parcial — primeiro incremento instalado**, substituindo apenas o estado planeado/preparado deste recorte; ACT-02 a 16 não foram implementadas nesta entrega. Instalado por avanço normal de main e pipeline existente em https://teste-investidor.tempo.co.mz/. Entrada: Gestão → Clusters → Editar → Competitividade — cinco forças de Porter. Preparação qualitativa por sector/região, evidências/fontes/períodos/limites e oportunidades, transferência JSON e recuperação confirmada do mesmo cluster. Sem gravação no servidor nem publicação da análise. Persistência, cobertura factual, território canónico e consulta pública continuam pendentes; esta instalação não encerra o critério amplo ACT-01.
Provas: backup fresco, restauro isolado sem rede das 176 tabelas; pipeline com lifecycle de schema (3.106 asserções), 59 testes Unit/320 asserções e 1.180 testes frontend sem falhas, tipos e compilação; quatro ficheiros do incremento conferidos por SHA-256 no instalado; C132 no destino passou. Comparação após instalação: todas as 176 tabelas de domínio preservadas, diferenças operacionais apenas em cache/sessões; 369 ficheiros/configuração idênticos. Acesso anónimo ao catálogo privado recusado HTTP401. Zero migrações/importações, nenhuma nova divulgação nem mudança de permissões; versão anterior 332c16eb preservada. Conferência visual autenticada permanece pendente. Recibo: `docs/investmoz/unification/competitiveness-sequential-qa-20261029.json`, em docs/investmoz/unification. As provas aceites anteriores foram reutilizadas; não foram reabertas por esta entrega.
### 29/10/2026 — ACT-01, persistência e aprovação: execução em curso
Ordem confirmada pela Tempo: 1) gravar/validar/recuperar na gestão com permissões e histórico, 2) consultar publicamente apenas conteúdo aprovado, 3) restantes alterações. A instalação da interface 23ecccfa e os 1.180 testes anteriores são provas desse incremento, não prova de persistência. Sem perda nova confirmada; preservar a versão pública anterior e os recibos de backup/restauro. O problema dos filtros provinciais permanece **separado e aberto**, sem declaração de correcção por estes trabalhos.
Escolha técnica: reutilizar snapshots cifrados nos eventos append-only de auditoria, já usados pelo portal; bloqueio transaccional do cluster, controlo de versão e histórico imutável. Sem alteração de schema/permissões. Guardar rascunho e recuperar histórico não publica; aprovação explícita fixa a versão consultável, edições seguintes preservam a aprovação anterior até nova decisão. Rascunhos parciais são guardáveis; aprovação exige os fundamentos das cinco forças. Recuperação/conflito não deve sobrescrever edições inadvertidamente. Acesso público continua limitado às fichas publicadas. Implementação e provas específicas de persistência/consulta ainda em preparação; restantes ACT mantêm o estado anterior.
### ACT-01 — prova específica antes da instalação, 29/10/2026
Sete testes Feature/93 asserções passaram na base restaurada competitiveness_persistence_20261029_test, porta 55441: gravação e leitura HTTP entre pedidos, retorno pela edição da gestão, histórico cifrado, validação de campos/cluster/forças, normalização do formato recuperável, conflitos sem sobrescrita, aprovação fixa, rascunhos posteriores sem divulgação, recuperação histórica como nova versão, retirada e exclusão do conteúdo não aprovado na consulta pública. Utilizadores sem permissão e pedidos anónimos foram recusados na gestão. O teste público percorreu a ficha publicada e confirmou 404 da ficha não publicada. Zero escritas na base activa. Esta prova não afirma conferência visual autenticada nem correcção dos filtros provinciais, que permanecem abertos. Recibo: docs/investmoz/unification/competitiveness-persistence-qa-20261029.json. Compilação QA, tipos e quatro testes frontend dirigidos passaram; instalação ainda pendente.
### 29/10/2026 — ACT-01, gravação/recuperação e consulta aprovada instaladas: b81f477b
Instalado pelo avanço normal de main e pipeline existente, preservando 23ecccfa para retorno. Gestão → Clusters → Editar → Competitividade agora recupera o rascunho do servidor e permite gravar, consultar a versão actual, confirmar recuperação e recuperar uma versão histórica como nova gravação. Versões cifradas e imutáveis na auditoria existente, com bloqueio transaccional do cluster e controlo de versão; conflitos conservam a versão guardada e os campos do editor. Reutilizada a permissão existente de gestão, sem novos papéis ou aprovadores. Rascunhos incompletos podem ser guardados; a aprovação exige âmbito e fundamentos das cinco forças.
**Percurso da gestão:** preencher → Guardar rascunho no servidor → voltar à edição para recuperar → consultar histórico quando necessário. Para comparar com outra gravação, usar Consultar versão actual do servidor e confirmar antes de substituir os campos; transferir primeiro edições que se queira conservar. Recuperar o histórico cria nova versão e mantém o histórico e a aprovação pública existente. A aprovação ou retirada é expressa, com evidência/motivo registado. A ficha pública de um cluster publicado mostra exclusivamente a análise aprovada; novas gravações privadas não alteram essa versão. Uma ficha não publicada continua indisponível ao público. Não houve aprovação nem divulgação automática de análises.
**Prova específica:** sete testes Feature/93 asserções na base restaurada e isolada competitiveness_persistence_20261029_test, porta55441. Abrange gravação/recuperação HTTP, retorno na gestão, validação e formato, histórico cifrado, conflito, acesso recusado, aprovação fixa, rascunho posterior privado, recuperação, retirada e consulta pública/404 de ficha não publicada. Os 1.180 testes frontend não são apresentados como prova substituta de persistência. Pipeline: tipos, compilação, lifecycle de schema1/3106, Unit59/320 e frontend1180 sem falhas. Dez ficheiros de código/testes do instalado conferidos por SHA-256 com os da QA; C132 no destino passou.
**Preservação e limites:** backup fresco/restauro isolado176; comparação posterior de176 tabelas sem alterações de domínio, apenas diferenças operacionais de cache/sessões; 369 ficheiros/configuração idênticos. Ligação real teste_investidor_app conferida em leitura, com privilégios existentes de SELECT/INSERT em auditoria e bloqueio do cluster; endpoint de gestão anónimo HTTP401. Nenhuma escrita sintética na base activa, migração, importação, mudança de permissões ou nova publicação. A conferência visual autenticada no instalado permanece pendente; não foi simulada uma decisão real de conteúdo no activo. ACT-01 tem agora persistência e consulta aprovada implementadas/instaladas, mantendo parcial a cobertura factual por sector/região. Recibo: docs/investmoz/unification/competitiveness-persistence-qa-20261029.json.
**Filtros provinciais: separado e aberto.** Esta entrega não alterou a lógica dos filtros nem demonstra a correcção relatada de Agronegócio/Maputo Província no portal. Preservam-se as provas e os estados históricos; não encerrar V2-07/V2-10 ou ACT-02 por causa da persistência de Competitividade. Restantes alterações permanecem na sequência posterior.
Origem de continuidade: C:/laragon/www/portal-investidor-competitiveness-qa-20261029, ramo codex/competitiveness-persistence-20261029, instalado b81f477b. A pasta principal anterior não deve substituir esta versão sem reconciliação do delta.
File diff suppressed because one or more lines are too long
@@ -1,72 +0,0 @@
import { forceLabels, forces } from '@/lib/cluster-competitiveness';
import type { CompetitivenessDraft } from '@/lib/cluster-competitiveness';
export type ApprovedCompetitiveness = {
draft: CompetitivenessDraft;
version: number;
at: string;
};
const pressures = {
low: 'Baixa',
medium: 'Média',
high: 'Alta',
'': 'Indeterminada',
};
export default function ClusterCompetitivenessPublic({
analysis,
}: {
analysis: ApprovedCompetitiveness;
}) {
return (
<section
className="space-y-3 rounded border p-4"
aria-label="Competitividade aprovada"
>
<h2 className="text-xl font-semibold">
Competitividade — cinco forças de Porter
</h2>
<p>
{analysis.draft.sector} · {analysis.draft.region} · Versão
aprovada {analysis.version} · {analysis.at}
</p>
<p>
Apreciação qualitativa fundamentada nas fontes e períodos
indicados. Não representa garantia de procura ou viabilidade.
</p>
{forces.map((force) => {
const row = analysis.draft.forces[force];
return (
<details key={force} className="rounded border p-3">
<summary>
{forceLabels[force]} · Pressão{' '}
{pressures[row.pressure]}
</summary>
<dl className="space-y-2">
{(
[
['evidence', 'Evidências'],
['source', 'Fonte e referência'],
['period', 'Período'],
['rationale', 'Justificação'],
['limitations', 'Limitações'],
[
'opportunity',
'Oportunidade e fundamento',
],
] as const
).map(([field, label]) => (
<div key={field}>
<dt className="font-semibold">{label}</dt>
<dd className="whitespace-pre-wrap">
{row[field]}
</dd>
</div>
))}
</dl>
</details>
);
})}
</section>
);
}
-107
View File
@@ -1,107 +0,0 @@
export const forceLabels = {
rivalry: 'Rivalidade entre concorrentes',
entrants: 'Ameaça de novos entrantes',
substitutes: 'Ameaça de produtos ou serviços substitutos',
suppliers: 'Poder negocial dos fornecedores',
buyers: 'Poder negocial dos clientes',
} as const;
export type Force = keyof typeof forceLabels;
export type Assessment = {
pressure: '' | 'low' | 'medium' | 'high';
evidence: string;
source: string;
period: string;
rationale: string;
limitations: string;
opportunity: string;
};
export type CompetitivenessDraft = {
version: 1;
clusterId: string;
sector: string;
region: string;
forces: Record<Force, Assessment>;
};
export const forces = Object.keys(forceLabels) as Force[];
const fields = [
'evidence',
'source',
'period',
'rationale',
'limitations',
'opportunity',
] as const;
export function newCompetitivenessDraft(
clusterId: string,
sector: string,
): CompetitivenessDraft {
return {
version: 1,
clusterId,
sector,
region: '',
forces: Object.fromEntries(
forces.map((force) => [
force,
{
pressure: '',
evidence: '',
source: '',
period: '',
rationale: '',
limitations: '',
opportunity: '',
},
]),
) as Record<Force, Assessment>,
};
}
// A pressure is an author's qualitative assessment, never inferred from missing data.
export function assessmentReady(row: Assessment): boolean {
return (
['low', 'medium', 'high'].includes(row.pressure) &&
fields.every((field) => row[field].trim().length > 0)
);
}
export function diagnosticReady(draft: CompetitivenessDraft): boolean {
return (
Boolean(draft.sector.trim() && draft.region.trim()) &&
forces.every((force) => assessmentReady(draft.forces[force]))
);
}
export function readCompetitivenessDraft(
raw: string,
clusterId: string,
): CompetitivenessDraft {
if (raw.length > 250_000)
throw Error('O ficheiro excede o limite de 250 KB.');
const input = JSON.parse(raw);
if (!input || input.version !== 1 || input.clusterId !== clusterId) {
throw Error(
'O ficheiro não corresponde a este cluster ou tem uma versão incompatível.',
);
}
const text = (value: unknown, max: number): string => {
if (typeof value !== 'string' || value.length > max)
throw Error('O ficheiro contém campos inválidos.');
return value;
};
const draft = newCompetitivenessDraft(clusterId, text(input.sector, 200));
draft.region = text(input.region, 1000);
for (const force of forces) {
const row = input.forces?.[force];
if (!row || !['', 'low', 'medium', 'high'].includes(row.pressure)) {
throw Error('O ficheiro não contém as cinco forças válidas.');
}
draft.forces[force].pressure = row.pressure;
for (const field of fields)
draft.forces[force][field] = text(row[field], 5000);
}
return draft;
}
-4
View File
@@ -11,7 +11,6 @@ import { Management } from '../management/shared';
import { Layout, Field, control, button, labels, kind } from './shared';
import type { Dossier } from './shared';
import InvestmentComplements, { type ComplementOption } from '@/components/investment-complements';
import ClusterCompetitivenessPublic, { type ApprovedCompetitiveness } from '@/components/cluster-competitiveness-public';
export default function Show(props: Props) {
return (
<Editor
@@ -22,7 +21,6 @@ export default function Show(props: Props) {
}
type Props = {
competitiveness?: ApprovedCompetitiveness | null;
complements?: ComplementOption[];
geography: StructureGeography;
existingSubmissions?: { id: string; version: number; updated_at: string }[];
@@ -56,7 +54,6 @@ type Props = {
};
function Editor({
competitiveness,
complements = [],
record,
related,
@@ -97,7 +94,6 @@ function Editor({
{kind(record)} · {record.content.code}
</p>
<p className="text-sm">Fonte: {record.source}</p>
{competitiveness && <ClusterCompetitivenessPublic analysis={competitiveness} />}
{!management && (
<div className="space-y-2 rounded-xl border p-4">
<Link className="underline" href={portfolioPreparationHref(record.id, pageUrl)}>
@@ -1,501 +0,0 @@
import { useState } from 'react';
import { portalRequest, PortalApiError } from '@/lib/portal-api';
import {
assessmentReady,
diagnosticReady,
forceLabels,
forces,
newCompetitivenessDraft,
readCompetitivenessDraft,
} from '@/lib/cluster-competitiveness';
import type {
Assessment,
CompetitivenessDraft,
Force,
} from '@/lib/cluster-competitiveness';
import { button, control } from '../management/shared';
const labels = {
evidence: 'Observações e evidências locais',
source: 'Fonte e referência recuperável (documento, página ou endereço)',
period: 'Período dos dados e data da consulta',
rationale: 'Justificação da pressão atribuída',
limitations: 'Limitações, cobertura e pressupostos',
opportunity: 'Oportunidade de investimento e fundamento',
} as const;
export type CompetitivenessState = {
version: number;
draft: CompetitivenessDraft | null;
approved: {
draft: CompetitivenessDraft;
version: number;
at: string;
} | null;
history: { version: number; action: string; at: string; actor: number }[];
};
const actions: Record<string, string> = {
saved: 'Gravação',
approved: 'Aprovação pública',
withdrawn: 'Retirada da aprovação',
restored: 'Recuperação do histórico',
};
export default function Competitiveness({
clusterId,
sector,
initial,
}: {
clusterId: string;
sector: string;
initial?: CompetitivenessState | null;
}) {
const [draft, setDraft] = useState(
() => initial?.draft ?? newCompetitivenessDraft(clusterId, sector),
);
const [server, setServer] = useState<CompetitivenessState>(
initial ?? { version: 0, draft: null, approved: null, history: [] },
);
const [serverPending, setServerPending] =
useState<CompetitivenessState | null>(null);
const [restoreVersion, setRestoreVersion] = useState<number | null>(null);
const [busy, setBusy] = useState(false);
const [reason, setReason] = useState('');
const [pending, setPending] = useState<CompetitivenessDraft | null>(null);
const [message, setMessage] = useState('');
const dirty =
JSON.stringify(draft) !==
JSON.stringify(
server.draft ?? newCompetitivenessDraft(clusterId, sector),
);
const endpoint = `/management/clusters/${clusterId}/competitiveness`;
function errorMessage(error: unknown) {
if (error instanceof PortalApiError) {
if (error.status === 409)
return 'Conflito de versão. Os campos actuais foram conservados. Transfira o rascunho e consulte a versão do servidor antes de reconciliar.';
if ([401, 403, 419].includes(error.status))
return 'Sessão ou permissão indisponível. Os campos foram conservados; transfira o rascunho antes de autenticar-se novamente.';
if (error.status === 422)
return (
Object.values(error.errors).flat().join(' ') ||
error.message
);
}
return 'Não foi possível concluir a operação; os campos actuais foram conservados.';
}
function apply(state: CompetitivenessState) {
setServer(state);
setDraft(state.draft ?? newCompetitivenessDraft(clusterId, sector));
setPending(null);
setServerPending(null);
setRestoreVersion(null);
}
async function command(
action: string,
extra: Record<string, unknown> = {},
) {
setBusy(true);
try {
const state = await portalRequest<CompetitivenessState>(
`${endpoint}/${action}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
expected_version: server.version,
...extra,
}),
},
);
apply(state);
setMessage(
`${actions[action]} concluída no servidor. Versão ${state.version}.`,
);
} catch (error) {
setMessage(errorMessage(error));
} finally {
setBusy(false);
}
}
function change(force: Force, field: keyof Assessment, value: string) {
setDraft((previous) => ({
...previous,
forces: {
...previous.forces,
[force]: { ...previous.forces[force], [field]: value },
},
}));
}
function download() {
const url = URL.createObjectURL(
new Blob([JSON.stringify(draft, null, 2)], {
type: 'application/json',
}),
);
const link = document.createElement('a');
link.href = url;
link.download = `competitividade-${clusterId}.json`;
link.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
setMessage(
'Ficheiro preparado para transferência. Conserve-o para recuperar a análise.',
);
}
return (
<section
className="max-w-3xl space-y-4 rounded border p-4"
aria-label="Diagnóstico de competitividade"
>
<h2 className="text-xl font-semibold">
Competitividade — cinco forças de Porter
</h2>
<p>
Análise qualitativa por sector e região ligada a este cluster.
Descreva as evidências e justifique cada pressão; não existe
pontuação automática nem média entre forças.
</p>
<p>
Guarde o rascunho para o recuperar no servidor. A publicação
exige aprovação expressa; a classificação representa a
apreciação do autor.
</p>
<p role="status">
Versão no servidor: {server.version}.{' '}
{dirty
? 'Existem alterações por guardar.'
: 'Campos correspondem à versão recuperada.'}{' '}
{server.approved
? `Consulta pública: versão aprovada ${server.approved.version}; novas edições conservam essa versão até outra aprovação.`
: 'Sem análise aprovada para consulta pública.'}
</p>
<fieldset disabled={busy} className="space-y-4">
<label className="block">
Sector analisado
<input
className={control}
maxLength={200}
value={draft.sector}
onChange={(e) =>
setDraft({ ...draft, sector: e.target.value })
}
/>
</label>
{draft.sector !== sector && (
<p>
O sector da análise difere do sector actual do cluster (
{sector}). Confira o âmbito antes de reutilizar
conclusões.
</p>
)}
<label className="block">
Região / província / distrito e abrangência
<input
className={control}
maxLength={1000}
value={draft.region}
onChange={(e) =>
setDraft({ ...draft, region: e.target.value })
}
/>
</label>
<p role="status">
{diagnosticReady(draft)
? 'Cinco forças preenchidas para revisão; fontes e conclusões requerem leitura do autor.'
: 'Diagnóstico incompleto: indique o âmbito e complete as evidências das cinco forças.'}
</p>
{forces.map((force) => (
<details key={force} className="rounded border p-3">
<summary className="cursor-pointer font-semibold">
{forceLabels[force]} ·{' '}
{assessmentReady(draft.forces[force])
? 'Preenchida para revisão'
: 'Indeterminada / incompleta'}
</summary>
<div className="space-y-3 pt-3">
<label className="block">
Pressão competitiva
<select
className={control}
value={draft.forces[force].pressure}
onChange={(e) =>
change(
force,
'pressure',
e.target.value,
)
}
>
<option value="">Indeterminada</option>
<option value="low">Baixa</option>
<option value="medium">Média</option>
<option value="high">Alta</option>
</select>
</label>
{Object.entries(labels).map(([field, label]) => (
<label key={field} className="block">
{label}
<textarea
className={control}
rows={3}
maxLength={5000}
value={
draft.forces[force][
field as keyof typeof labels
]
}
onChange={(e) =>
change(
force,
field as keyof typeof labels,
e.target.value,
)
}
/>
</label>
))}
</div>
</details>
))}
<button type="button" className={button} onClick={download}>
Transferir análise / rascunho
</button>
<label className="block">
Recuperar ficheiro deste cluster
<input
className={control}
type="file"
accept=".json,application/json"
onChange={async (e) => {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) return;
setPending(null);
try {
if (file.size > 250_000)
throw Error(
'O ficheiro excede o limite de 250 KB.',
);
setPending(
readCompetitivenessDraft(
await file.text(),
clusterId,
),
);
setMessage(
'Ficheiro lido. Confira o âmbito e confirme a substituição dos campos actuais.',
);
} catch (error) {
setMessage(
error instanceof Error
? error.message
: 'Não foi possível ler o ficheiro.',
);
}
}}
/>
</label>
{pending && (
<div className="space-y-2 rounded border p-3">
<p>
Recuperar análise de{' '}
{pending.sector || 'sector não indicado'} em{' '}
{pending.region || 'região não indicada'}? Os campos
actuais serão substituídos. Transfira primeiro o
rascunho actual se o quiser conservar.
</p>
<button
type="button"
className={button}
onClick={() => {
setDraft(pending);
setPending(null);
setMessage('Análise recuperada para edição.');
}}
>
Confirmar recuperação
</button>{' '}
<button
type="button"
className={button}
onClick={() => {
setPending(null);
setMessage(
'Recuperação cancelada; campos actuais preservados.',
);
}}
>
Cancelar
</button>
</div>
)}
{message && <p role="status">{message}</p>}
<div className="flex flex-wrap gap-2">
<button
type="button"
className={button}
disabled={!dirty}
onClick={() => command('saved', { draft })}
>
Guardar rascunho no servidor
</button>
<button
type="button"
className={button}
onClick={async () => {
setBusy(true);
try {
setServerPending(
await portalRequest<CompetitivenessState>(
endpoint,
),
);
setMessage(
'Versão do servidor lida. Confirme antes de substituir os campos.',
);
} catch (error) {
setMessage(errorMessage(error));
} finally {
setBusy(false);
}
}}
>
Consultar versão actual do servidor
</button>
</div>
{serverPending && (
<div className="rounded border p-3">
<p>
Versão {serverPending.version}:{' '}
{serverPending.draft?.sector ||
'Sector não indicado'}{' '}
/{' '}
{serverPending.draft?.region ||
'Região não indicada'}
. Recuperar substitui os campos actuais. Transfira
primeiro alterações que queira conservar.
</p>
<button
type="button"
className={button}
onClick={() => {
apply(serverPending);
setMessage(
'Versão actual recuperada do servidor.',
);
}}
>
Confirmar recuperação do servidor
</button>{' '}
<button
type="button"
className={button}
onClick={() => setServerPending(null)}
>
Cancelar
</button>
</div>
)}
<details className="rounded border p-3">
<summary>
Histórico no servidor (últimas 50 versões)
</summary>
{!server.history.length && (
<p>Ainda não existem gravações.</p>
)}
{server.history.map((entry) => (
<p key={entry.version}>
Versão {entry.version} ·{' '}
{actions[entry.action] ?? entry.action} · {entry.at}{' '}
· Utilizador {entry.actor}{' '}
<button
type="button"
className="underline"
onClick={() => setRestoreVersion(entry.version)}
>
Recuperar como rascunho
</button>
</p>
))}
{restoreVersion !== null && (
<div>
<p>
Recuperar a versão {restoreVersion} cria uma
nova versão no servidor e substitui os campos
actuais. A versão pública aprovada mantém-se.
Transfira primeiro alterações por guardar.
</p>
<button
type="button"
className={button}
onClick={() =>
command('restored', {
revision: restoreVersion,
})
}
>
Confirmar recuperação do histórico
</button>{' '}
<button
type="button"
className={button}
onClick={() => setRestoreVersion(null)}
>
Cancelar
</button>
</div>
)}
</details>
<details className="rounded border p-3">
<summary>Aprovação para consulta pública</summary>
<p>
Aprovar torna a versão guardada consultável na ficha
pública deste cluster, quando essa ficha estiver
publicada. Guardar um rascunho não o publica. A
evidência de aprovação e as identidades do histórico
ficam na gestão.
</p>
<label className="block">
Evidência / motivo da aprovação ou retirada
<textarea
className={control}
maxLength={3000}
value={reason}
onChange={(e) => setReason(e.target.value)}
/>
</label>
<button
type="button"
className={button}
disabled={
dirty ||
!diagnosticReady(draft) ||
server.version === 0 ||
reason.trim().length < 10
}
onClick={() => command('approved', { reason })}
>
Aprovar versão guardada para consulta pública
</button>{' '}
<button
type="button"
className={button}
disabled={
dirty ||
!server.approved ||
reason.trim().length < 10
}
onClick={() => command('withdrawn', { reason })}
>
Retirar aprovação pública
</button>
{dirty && (
<p>
Guarde primeiro as alterações ou recupere a versão
pretendida antes de decidir a aprovação.
</p>
)}
</details>
</fieldset>
</section>
);
}
-10
View File
@@ -6,8 +6,6 @@ import { Management, Field, button, control } from '../management/shared';
import { titles, statuses, stages, roles } from './types';
import type { Structure, StructureRecord, Chain } from './types';
import ValueNetwork from './value-network';
import Competitiveness from './competitiveness';
import type { CompetitivenessState } from './competitiveness';
import type { ChainLink, Gap } from './value-network';
function ChainForm({ clusterId, chain }: { clusterId: string; chain?: Chain }) {
@@ -77,7 +75,6 @@ export default function Editor({
memberships,
chainLinks,
gaps,
competitiveness,
}: {
structure: Structure;
record: StructureRecord | null;
@@ -86,7 +83,6 @@ export default function Editor({
chains: Chain[];
chainLinks: ChainLink[];
gaps: Gap[];
competitiveness?: CompetitivenessState | null;
memberships: {
id: string;
project_id: string;
@@ -302,12 +298,6 @@ export default function Editor({
</form>
{isCluster && record && (
<>
<Competitiveness
key={record.id}
clusterId={record.id}
sector={record.sector ?? ''}
initial={competitiveness}
/>
<section className="max-w-3xl space-y-4">
<h2 className="text-xl font-semibold">
Projectos associados
-2
View File
@@ -254,8 +254,6 @@ Route::middleware(['auth', 'verified', \App\Http\Middleware\EnsureScopedPortalAd
Route::get('portfolio/{id}', [InvestmentPortfolioController::class, 'show'])->whereUuid('id')->name('portfolio.show');
Route::put('portfolio/{dossier}', [InvestmentPortfolioController::class, 'save'])->name('portfolio.save');
Route::post('portfolio/{dossier}/publish', [InvestmentPortfolioController::class, 'publish'])->name('portfolio.publish');
Route::get('clusters/{cluster}/competitiveness', [\App\Http\Controllers\ClusterCompetitivenessController::class, 'show'])->name('clusters.competitiveness.show');
Route::post('clusters/{cluster}/competitiveness/{action}', [\App\Http\Controllers\ClusterCompetitivenessController::class, 'change'])->whereIn('action', ['saved', 'approved', 'withdrawn', 'restored'])->name('clusters.competitiveness.change');
Route::post('clusters/{cluster}/chain-links', [ClusterValueChainController::class, 'link'])->name('clusters.chain-links.store');
Route::delete('clusters/{cluster}/chain-links/{link}', [ClusterValueChainController::class, 'removeLink'])->name('clusters.chain-links.delete');
Route::post('clusters/{cluster}/gaps', [ClusterValueChainController::class, 'gap'])->name('clusters.gaps.store');
@@ -1,175 +0,0 @@
<?php
namespace Tests\Feature;
use App\Models\EconomicCluster;
use App\Models\InvestmentDossier;
use App\Models\User;
use App\Services\ClusterCompetitiveness;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Inertia\Testing\AssertableInertia;
use Tests\TestCase;
class ClusterCompetitivenessTest extends TestCase
{
use DatabaseTransactions;
protected function setUp(): void
{
parent::setUp();
$this->withoutVite();
}
private function manager(): User
{
$user = User::factory()->create();
config(['portal.managers' => [$user->email], 'portal.profile_access_enabled' => false]);
$this->actingAs($user);
return $user;
}
private function cluster(): EconomicCluster
{
return EconomicCluster::create(['code' => 'COMP-'.bin2hex(random_bytes(6)), 'name' => 'Cluster de teste de persistência', 'sector' => 'Energia', 'status' => 'planned']);
}
private function draft(EconomicCluster $cluster): array
{
$draft = ['version' => 1, 'clusterId' => $cluster->id, 'sector' => 'Energia', 'region' => 'Distrito de demonstração', 'forces' => []];
foreach (ClusterCompetitiveness::FORCES as $force) {
$draft['forces'][$force] = ['pressure' => 'medium', 'evidence' => 'Observação de teste, não publicável como facto real',
'source' => 'Fonte de demonstração p. 2', 'period' => '2025', 'rationale' => 'Apreciação de teste',
'limitations' => 'Dados sintéticos', 'opportunity' => 'Hipótese de demonstração'];
}
return $draft;
}
private function url(EconomicCluster $cluster): string
{
return '/management/clusters/'.$cluster->id.'/competitiveness';
}
public function test_save_recovers_from_database_across_requests_and_records_encrypted_history(): void
{
$actor = $this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])
->assertOk()->assertJsonPath('version', 1)->assertJsonPath('draft', $draft)->assertJsonPath('approved', null);
$this->getJson($this->url($cluster))->assertOk()->assertHeader('Cache-Control', 'no-store, private')
->assertJsonPath('draft', $draft)->assertJsonPath('history.0.actor', $actor->id)->assertJsonPath('history.0.action', 'saved');
$this->get('/management/clusters/'.$cluster->id.'/edit')->assertInertia(fn (AssertableInertia $page) => $page
->component('structures/editor')->where('competitiveness.version', 1)->where('competitiveness.draft.region', $draft['region']));
$event = DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $cluster->id)->first();
$this->assertStringNotContainsString($draft['forces']['rivalry']['evidence'], $event->after_data);
$this->assertSame('restricted', $event->data_classification);
$this->assertNull(app(ClusterCompetitiveness::class)->publicData($cluster->id));
}
public function test_validation_conflict_and_server_error_do_not_overwrite_the_saved_version(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk();
$changed = $draft;
$changed['region'] = 'Outra região';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $changed])->assertConflict();
$changed['forces']['buyers']['pressure'] = 'zero';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $changed])->assertUnprocessable();
$changed = $draft;
$changed['clusterId'] = $this->cluster()->id;
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $changed])->assertUnprocessable();
$this->getJson($this->url($cluster))->assertJsonPath('version', 1)->assertJsonPath('draft', $draft)->assertJsonCount(1, 'history');
}
public function test_approval_pins_content_and_later_drafts_and_restoration_do_not_publish(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$url = $this->url($cluster);
$this->postJson($url.'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk();
$this->postJson($url.'/approved', ['expected_version' => 1, 'reason' => 'Aprovação sintética em QA'])->assertOk()->assertJsonPath('version', 2);
$changed = $draft;
$changed['region'] = 'Rascunho privado posterior';
$this->postJson($url.'/saved', ['expected_version' => 2, 'draft' => $changed])->assertOk();
$public = app(ClusterCompetitiveness::class)->publicData($cluster->id);
$this->assertSame($draft, $public['draft']);
$this->assertSame(['draft', 'version', 'at'], array_keys($public));
$this->postJson($url.'/restored', ['expected_version' => 3, 'revision' => 1])->assertOk()
->assertJsonPath('version', 4)->assertJsonPath('draft', $draft)->assertJsonPath('approved.version', 2);
$this->assertSame($public, app(ClusterCompetitiveness::class)->publicData($cluster->id));
$this->postJson($url.'/withdrawn', ['expected_version' => 4, 'reason' => 'Retirada sintética em QA'])->assertOk()->assertJsonCount(5, 'history');
$this->assertNull(app(ClusterCompetitiveness::class)->publicData($cluster->id));
$this->getJson($url)->assertJsonPath('draft', $draft);
}
public function test_incomplete_drafts_are_saved_but_not_approved_and_unknown_revisions_do_not_write(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$draft['forces']['buyers']['source'] = '';
$draft['forces']['entrants']['pressure'] = '';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])->assertOk();
$this->postJson($this->url($cluster).'/approved', ['expected_version' => 1, 'reason' => 'Pedido de aprovação QA'])->assertUnprocessable();
$this->postJson($this->url($cluster).'/restored', ['expected_version' => 1, 'revision' => 99])->assertNotFound();
$this->getJson($this->url($cluster))->assertJsonPath('version', 1)->assertJsonPath('draft.forces.buyers.source', '');
}
public function test_saved_data_keeps_the_portable_file_contract_and_rejects_missing_forces(): void
{
$this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$draft['version'] = '1';
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 0, 'draft' => $draft])
->assertOk()->assertJsonPath('draft.version', 1);
unset($draft['forces']['suppliers']);
$this->postJson($this->url($cluster).'/saved', ['expected_version' => 1, 'draft' => $draft])->assertUnprocessable();
$this->getJson($this->url($cluster))->assertJsonPath('version', 1);
}
public function test_anonymous_and_unauthorised_users_cannot_read_history_or_mutate(): void
{
$cluster = $this->cluster();
$url = $this->url($cluster);
$this->getJson($url)->assertUnauthorized();
$this->postJson($url.'/saved', [])->assertUnauthorized();
$this->actingAs(User::factory()->create());
$this->getJson($url)->assertForbidden();
foreach (['saved', 'approved', 'withdrawn', 'restored'] as $action) {
$this->postJson($url.'/'.$action, [])->assertForbidden();
}
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'cluster-competitiveness')->where('entity_id', $cluster->id)->count());
}
public function test_public_dossier_returns_only_the_approved_projection_and_unpublished_dossiers_remain_hidden(): void
{
$actor = $this->manager();
$cluster = $this->cluster();
$draft = $this->draft($cluster);
$dossier = InvestmentDossier::create(['cluster_id' => $cluster->id, 'title' => 'Ficha sintética QA', 'source' => 'Fonte sintética QA',
'content' => ['sector' => 'Energia'], 'publication_status' => 'published', 'approval_basis' => 'document_owner',
'approval_evidence' => 'Aprovação sintética só na base QA', 'published_at' => now()]);
$service = app(ClusterCompetitiveness::class);
$service->change($cluster, $actor->id, 0, 'saved', ['draft' => $draft]);
$this->app['auth']->forgetGuards();
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page->where('competitiveness', null));
$service->change($cluster, $actor->id, 1, 'approved', ['reason' => 'Aprovação privada de teste']);
$draft['region'] = 'CONTEÚDO PRIVADO NÃO APROVADO';
$service->change($cluster, $actor->id, 2, 'saved', ['draft' => $draft]);
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page
->where('competitiveness.version', 2)->where('competitiveness.draft.region', 'Distrito de demonstração')
->missing('competitiveness.actor')->missing('competitiveness.evidence')->missing('competitiveness.history'));
$dossier->update(['publication_status' => 'draft']);
$this->get('/carteira/'.$dossier->id)->assertNotFound();
$dossier->update(['publication_status' => 'published']);
$service->change($cluster, $actor->id, 3, 'withdrawn', ['reason' => 'Retirada privada de teste']);
$this->get('/carteira/'.$dossier->id)->assertInertia(fn (AssertableInertia $page) => $page->where('competitiveness', null));
}
}
@@ -1,101 +0,0 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
assessmentReady,
diagnosticReady,
forces,
newCompetitivenessDraft,
readCompetitivenessDraft,
} from '../../resources/js/lib/cluster-competitiveness.ts';
function complete() {
const draft = newCompetitivenessDraft('cluster-a', 'Energia');
draft.region = 'Distrito de demonstração';
for (const force of forces)
Object.assign(draft.forces[force], {
pressure: 'medium',
evidence: 'Observação de demonstração',
source: 'Fonte de demonstração, p. 2',
period: '2025',
rationale: 'Pressão fundamentada na observação',
limitations: 'Amostra parcial',
opportunity: 'Investigar capacidade de armazenamento',
});
return draft;
}
test('ausência de dados permanece indeterminada nas cinco forças independentes', () => {
const draft = newCompetitivenessDraft('cluster-a', 'Energia');
assert.equal(forces.length, 5);
assert.equal(diagnosticReady(draft), false);
draft.forces.buyers.evidence = 'Observação';
assert.equal(draft.forces.suppliers.evidence, '');
assert.ok(forces.every((force) => draft.forces[force].pressure === ''));
});
test('classificação exige todos os fundamentos; âmbito é necessário para o diagnóstico', () => {
const draft = complete();
assert.equal(diagnosticReady(draft), true);
for (const field of [
'evidence',
'source',
'period',
'rationale',
'limitations',
'opportunity',
]) {
const row = { ...draft.forces.buyers, [field]: ' ' };
assert.equal(assessmentReady(row), false, field);
}
draft.region = '';
assert.equal(diagnosticReady(draft), false);
});
test('ficheiro conserva rascunhos, dados e incertezas sem modificar o original', () => {
const draft = complete();
draft.forces.entrants.pressure = '';
const restored = readCompetitivenessDraft(
JSON.stringify(draft),
'cluster-a',
);
assert.deepEqual(restored, draft);
restored.forces.buyers.source = 'Outra fonte';
assert.notEqual(restored.forces.buyers.source, draft.forces.buyers.source);
assert.equal(diagnosticReady(restored), false);
});
test('recuperação rejeita outro cluster, versões, forças ausentes e campos inválidos', () => {
assert.throws(() =>
readCompetitivenessDraft(JSON.stringify(complete()), 'cluster-b'),
);
for (const mutate of [
(d) => {
d.version = 2;
},
(d) => {
delete d.forces.buyers;
},
(d) => {
d.forces.buyers.pressure = 'zero';
},
(d) => {
d.forces.buyers.source = 42;
},
(d) => {
d.forces.buyers.source = 'x'.repeat(5001);
},
(d) => {
d.region = null;
},
]) {
const draft = complete();
mutate(draft);
assert.throws(() =>
readCompetitivenessDraft(JSON.stringify(draft), 'cluster-a'),
);
}
assert.throws(() => readCompetitivenessDraft('null', 'cluster-a'));
assert.throws(() =>
readCompetitivenessDraft('x'.repeat(250001), 'cluster-a'),
);
});