Compare commits

..
Author SHA1 Message Date
nala 669cb8979b fix: open individual sectors from catalogue cards without repeated detail
tests / ci (push) Canceled after 0s
2026-10-27 13:47:50 +02:00
nala 9fda6d6e12 test: make proactive API snapshot proof portable without local QA outputs
tests / ci (push) Canceled after 0s
2026-10-27 12:45:32 +02:00
9 changed files with 181 additions and 51 deletions
@@ -71,7 +71,6 @@ class PortalServicesController extends Controller
$props = ['section' => $page, 'filters' => $filters, 'countries' => app(CountryCatalogue::class)->all()];
if ($page === 'sectors') {
$props['sectors'] = \App\Services\SectorOrganization::catalog();
$props['sectorDetails'] = $props['sectors']->map(fn (Sector $sector) => app(\App\Services\SectorInvestment::class)->page($sector))->values();
}
if (in_array($page, ['documents', 'legislation'], true)) {
$query = $this->resources();
@@ -1403,3 +1403,16 @@ O relatório comum tinha um conflito de imports. Foram conservados ambos e as ch
Retomar na sessão local com SSH autenticado, usando a autorização já registada: medir espaço/inodes na partição efectiva, comparar com compilação e backup, conferir main e revisão activa, criar e ensaiar backup fresco, instalar L8 e conferir apenas o seu delta. Depois repetir as guardas necessárias para a candidata conjunta e conferir o delta proactivo preservando L8. Manter a versão activa até a nova estar pronta. Excluir fixtures QA dos inputs de instalação; não executar seeders/importações de amostras, não publicar pilotos privados nem usar o pacote QA como cópia da base activa. SGF TEMPO separado.
Recibo de preparação: `docs/investmoz/unification/sequential-delivery-preparation-20261027.json`. Provas e instrução de passagem: `outputs/sequential-delivery-20261027/`. Os recibos de execução e as duas revisões servidas serão registados apenas depois de execução real na sessão autenticada; este recibo não os substitui.
## 27/10/2026 — correcção delimitada da navegação dos sectores
Reprodução local autorizada da intenção de `88b39b527859bd420f2e223a7afea89bea38de54`, indisponível no origin/outro ambiente. Base exacta `9fda6d6e12832a3cf25e7314f1f895dd430238c5`; novo ramo `codex/sector-navigation-fix-local-20261027`. Não se afirma identidade binária com o pacote externo. QA concluída; instalação ainda pendente nesta revisão documental.
**Causa e alteração:** o índice `/sectores` carregava `sectorDetails` para todos os sectores e apresentava as páginas completas abaixo dos cartões; os cartões passavam a âncoras locais. Removidos esse carregamento e a apresentação agregada. Os cartões usam `sector.url`, preservando as páginas individuais, a especialização Fintech e os caminhos históricos existentes. O índice mantém descrições curtas, contagens e acesso aos sectores, sem repetir guias e oportunidades de todos eles.
**Preservação:** página individual, `SectorContent`, `SectorEntry`, `SectorInvestment`, `SectorOrganization`, filtros, dados mestres, originais e continuidade para o mesmo dossier não foram alterados. PEC `7fdb6b42` continua no seu ramo QA; não é integrado nesta correcção. L8 instalado e percurso proactivo de `9fda6d6e` preservados. Songo/Gurué privados; sem fixtures QA no activo, publicação, importações, migrations ou alterações de permissões.
**Delta demonstrado:** actualizados os dois testes afectados de `SectorCatalogueContentTest` e acrescentado um caso de isolamento/regresso/recarregamento:3 testes/319 asserções, base isolada `proactive_investment_20261027_test`, loopback55441, transacções com rollback; sem `migrate:fresh`. Wayfinder `--with-form`, TypeScript e build QA aprovados. Navegador real:10 cartões, acesso separado a Agronegócio e Turismo, oportunidades próprias, filtro Agronegócio mantido no catálogo, recarregamento da página individual e regresso ao índice sem repetição. Entrada Turismo para o dossier conserva o contexto sectorial na ligação e encaminha o visitante para a autenticação existente; não se repetiu a prova completa de gravação do dossier já aceite. A amostra QA não tem guias editoriais carregados; a preservação dos guias assenta na comparação dos consumidores/fontes, e será conferida no destino sem republicar conteúdo. A captura de imagem do browser devolveu «0 width»; árvore de acessibilidade e testes guardados, sem alegação de captura visual inexistente.
Requisitos existentes actualizados apenas neste delta: `MASTER-471012ab-0007` e `MASTER-9dee08d0-0017`; não se encerra automaticamente o âmbito mais amplo dessas linhas. Recibo QA: `docs/investmoz/unification/sector-navigation-qa-20261027.json`. Provas locais: `outputs/sector-navigation-fix-20261027/`. Entrega autorizada seguirá o processo Git existente, com capacidade, backup/restauro fresco, comparação de fonte/dados/schema/configuração/permissões e revisão efectivamente servida.
@@ -0,0 +1,88 @@
{
"date": "2026-10-27",
"status": "QA passed; not yet installed",
"source_intention": "88b39b527859bd420f2e223a7afea89bea38de54",
"reproduced_locally": true,
"byte_equivalence_to_external_commit": false,
"baseline": "9fda6d6e12832a3cf25e7314f1f895dd430238c5",
"branch": "codex/sector-navigation-fix-local-20261027",
"runtime_delta": [
"app/Http/Controllers/PortalServicesController.php",
"resources/js/pages/portal/services.tsx"
],
"tests": {
"feature_tests": 3,
"assertions": 319,
"database": "proactive_investment_20261027_test",
"port": 55441,
"rollback": true,
"routes": "passed",
"typescript": "passed",
"build": "passed"
},
"browser": {
"index_cards": 10,
"separate_sectors": [
"agronegocio",
"turismo"
],
"opportunities_isolated": true,
"filter": "agronegocio",
"back": true,
"reload": true,
"fintech_url_preserved": true,
"dossier_entry": "sector:3; existing guest authentication, no new dossier created",
"screenshots": "capture unavailable: zero width; AX proof retained",
"editorial_guides": "not loaded in QA sample; consumers unchanged, destination to check"
},
"protected_files": [
{
"file": "resources/js/components/sector-content.tsx",
"sha256_LF": "4ee55554f8a88ba7db1ac0dc74259d4e66603979c3873f9ec897437830b8e0ae",
"unchanged": true
},
{
"file": "resources/js/components/sector-entry.tsx",
"sha256_LF": "96672c709b70f840d105c664a92d49a17acb66cd852d43dadaa8f3fef8b3e845",
"unchanged": true
},
{
"file": "resources/js/components/public-sector-cards.tsx",
"sha256_LF": "858ba6235ef8b46461d416b3264436e4df2c9555b8e6d8c3644dbd8c55463484",
"unchanged": true
},
{
"file": "app/Services/SectorInvestment.php",
"sha256_LF": "0b8fbc6e3d03ede627e95332703e4749a53d86708bdc0f9579e958caf30e4d17",
"unchanged": true
},
{
"file": "app/Services/SectorOrganization.php",
"sha256_LF": "f0e54fe6acfe746c6758387f43610232cb51371b0404bcefaf466d8b704f3ae3",
"unchanged": true
},
{
"file": "app/Http/Controllers/PublicPortalController.php",
"sha256_LF": "cb6abd75533660a52e18148cc5f1ab706d2e08f9c94334510541e6b0e003b461",
"unchanged": true
},
{
"file": "resources/js/lib/project-dossier.ts",
"sha256_LF": "d59f3fa4dcb3fade1f967564c972e30932aa095a4e5dd4ba0be8b4e07492ef0a",
"unchanged": true
},
{
"file": "resources/js/lib/irpc-annual-settlement.ts",
"sha256_LF": "8cca5316259e3ebabfa75be9f64143a8664a447ac30b23b882318edfd70a68f1",
"unchanged": true
},
{
"file": "routes/web.php",
"sha256_LF": "95930c76977d446b74a422acc4b60869ff6bae5d4567c21cc644484c5f09cbda",
"unchanged": true
}
],
"pec_branch_not_integrated": "7fdb6b42",
"active_writes": 0,
"private_pilots_published": false
}
@@ -6924,3 +6924,16 @@ O relatório comum tinha um conflito de imports. Foram conservados ambos e as ch
Retomar na sessão local com SSH autenticado, usando a autorização já registada: medir espaço/inodes na partição efectiva, comparar com compilação e backup, conferir main e revisão activa, criar e ensaiar backup fresco, instalar L8 e conferir apenas o seu delta. Depois repetir as guardas necessárias para a candidata conjunta e conferir o delta proactivo preservando L8. Manter a versão activa até a nova estar pronta. Excluir fixtures QA dos inputs de instalação; não executar seeders/importações de amostras, não publicar pilotos privados nem usar o pacote QA como cópia da base activa. SGF TEMPO separado.
Recibo de preparação: `docs/investmoz/unification/sequential-delivery-preparation-20261027.json`. Provas e instrução de passagem: `outputs/sequential-delivery-20261027/`. Os recibos de execução e as duas revisões servidas serão registados apenas depois de execução real na sessão autenticada; este recibo não os substitui.
## 27/10/2026 — correcção delimitada da navegação dos sectores
Reprodução local autorizada da intenção de `88b39b527859bd420f2e223a7afea89bea38de54`, indisponível no origin/outro ambiente. Base exacta `9fda6d6e12832a3cf25e7314f1f895dd430238c5`; novo ramo `codex/sector-navigation-fix-local-20261027`. Não se afirma identidade binária com o pacote externo. QA concluída; instalação ainda pendente nesta revisão documental.
**Causa e alteração:** o índice `/sectores` carregava `sectorDetails` para todos os sectores e apresentava as páginas completas abaixo dos cartões; os cartões passavam a âncoras locais. Removidos esse carregamento e a apresentação agregada. Os cartões usam `sector.url`, preservando as páginas individuais, a especialização Fintech e os caminhos históricos existentes. O índice mantém descrições curtas, contagens e acesso aos sectores, sem repetir guias e oportunidades de todos eles.
**Preservação:** página individual, `SectorContent`, `SectorEntry`, `SectorInvestment`, `SectorOrganization`, filtros, dados mestres, originais e continuidade para o mesmo dossier não foram alterados. PEC `7fdb6b42` continua no seu ramo QA; não é integrado nesta correcção. L8 instalado e percurso proactivo de `9fda6d6e` preservados. Songo/Gurué privados; sem fixtures QA no activo, publicação, importações, migrations ou alterações de permissões.
**Delta demonstrado:** actualizados os dois testes afectados de `SectorCatalogueContentTest` e acrescentado um caso de isolamento/regresso/recarregamento:3 testes/319 asserções, base isolada `proactive_investment_20261027_test`, loopback55441, transacções com rollback; sem `migrate:fresh`. Wayfinder `--with-form`, TypeScript e build QA aprovados. Navegador real:10 cartões, acesso separado a Agronegócio e Turismo, oportunidades próprias, filtro Agronegócio mantido no catálogo, recarregamento da página individual e regresso ao índice sem repetição. Entrada Turismo para o dossier conserva o contexto sectorial na ligação e encaminha o visitante para a autenticação existente; não se repetiu a prova completa de gravação do dossier já aceite. A amostra QA não tem guias editoriais carregados; a preservação dos guias assenta na comparação dos consumidores/fontes, e será conferida no destino sem republicar conteúdo. A captura de imagem do browser devolveu «0 width»; árvore de acessibilidade e testes guardados, sem alegação de captura visual inexistente.
Requisitos existentes actualizados apenas neste delta: `MASTER-471012ab-0007` e `MASTER-9dee08d0-0017`; não se encerra automaticamente o âmbito mais amplo dessas linhas. Recibo QA: `docs/investmoz/unification/sector-navigation-qa-20261027.json`. Provas locais: `outputs/sector-navigation-fix-20261027/`. Entrega autorizada seguirá o processo Git existente, com capacidade, backup/restauro fresco, comparação de fonte/dados/schema/configuração/permissões e revisão efectivamente servida.
File diff suppressed because one or more lines are too long
+1 -34
View File
@@ -71,7 +71,6 @@ type Page<T> = {
};
type Props = {
sectorEntry?: SectorDetailData['sectorEntry'];
sectorDetails?: SectorDetailData[];
countries?: Country[];
section: string;
filters?: Record<string, string | string[]>;
@@ -518,41 +517,9 @@ export default function Services(p: Props) {
description: sector.short_description,
opportunitiesCount: sector.opportunities_count,
specializations: sector.specializations,
url: p.sectorDetails?.length
? `#sector-${sector.slug}`
: sector.url,
url: sector.url,
}))}
/>
{p.sectorDetails?.map((detail) => (
<section
id={`sector-${detail.sector.slug}`}
key={detail.sector.id}
aria-labelledby={`sector-heading-${detail.sector.slug}`}
className="scroll-mt-24 space-y-5 border-t border-slate-200 pt-6"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<h2
id={`sector-heading-${detail.sector.slug}`}
className="text-2xl font-bold"
>
{detail.sector.name}
</h2>
<Link
href={`/sectores/${detail.sector.slug}`}
className="inline-flex min-h-11 items-center text-sm font-semibold underline"
>
Abrir página do sector
</Link>
</div>
<SectorContent {...detail} headingLevel={3} />
<a
href="#sectores-indice"
className="inline-flex min-h-11 items-center text-sm font-semibold underline"
>
Voltar à lista de sectores
</a>
</section>
))}
{!p.sectors?.length && (
<p>Não existem sectores publicados.</p>
)}
+43 -12
View File
@@ -47,17 +47,22 @@ class SectorCatalogueContentTest extends TestCase
$before = [DB::table('desk_cases')->count(), DB::table('opportunities')->count(), DB::table('portal_resources')->count()];
$props = $this->get('/sectores')->assertOk()->inertiaProps();
$this->assertSame(array_column($props['sectors'], 'id'), array_column(array_column($props['sectorDetails'], 'sector'), 'id'));
foreach ($props['sectorDetails'] as $detail) {
$single = $this->get('/sectores/'.$detail['sector']['slug'])->assertOk()->inertiaProps();
$this->assertSame($single['sector'], $detail['sector']);
$this->assertSame($single['sectorEntry'], $detail['sectorEntry']);
$this->assertSame($single['opportunities'], $detail['opportunities']);
$this->assertIsArray($detail['sectorEntry']['articles']);
$this->assertIsArray($detail['sectorEntry']['relations']);
$this->assertIsArray($detail['sectorEntry']['locations']);
$this->assertArrayNotHasKey('sectorDetails', $props);
$ids = array_column($props['sectors'], 'id');
$this->assertSame($ids, array_values(array_unique($ids)));
foreach ($props['sectors'] as $card) {
$this->assertSame(\App\Services\SectorOrganization::href($card['slug']), $card['url']);
$this->assertStringNotContainsString('#', $card['url']);
$single = $this->get($card['url'])->assertOk()->inertiaProps();
$this->assertSame('sector', $single['section']);
$this->assertSame($card['id'], $single['sector']['id']);
$this->assertArrayNotHasKey('sectorDetails', $single);
$this->assertArrayNotHasKey('sectors', $single);
$this->assertIsArray($single['sectorEntry']['articles']);
$this->assertIsArray($single['sectorEntry']['relations']);
$this->assertIsArray($single['sectorEntry']['locations']);
}
$entry = collect($props['sectorDetails'])->firstWhere('sector.id', $withoutGuide->id)['sectorEntry'];
$entry = $this->get('/sectores/'.$withoutGuide->slug)->assertOk()->inertiaProps()['sectorEntry'];
$this->assertSame([], $entry['articles']);
$this->assertSame('/minhas-oportunidades/create?sector_id='.$withoutGuide->id, $entry['register_href']);
$this->assertNotNull($entry['start_href']);
@@ -80,10 +85,11 @@ class SectorCatalogueContentTest extends TestCase
}
config(['investor-path.enabled' => false, 'portal.profile_access_enabled' => false]);
$props = $this->get('/sectores')->assertOk()->inertiaProps();
$ids = array_column(array_column($props['sectorDetails'], 'sector'), 'id');
$this->assertArrayNotHasKey('sectorDetails', $props);
$ids = array_column($props['sectors'], 'id');
$this->assertNotContains($unpublished->id, $ids);
$this->assertNotContains($inactive->id, $ids);
$detail = collect($props['sectorDetails'])->firstWhere('sector.id', $public->id);
$detail = $this->get('/sectores/'.$public->slug)->assertOk()->inertiaProps();
$this->assertSame([$slugs['published']], array_column($detail['opportunities'], 'slug'));
$this->assertNull($detail['sectorEntry']['start_href']);
$this->assertNull($detail['sectorEntry']['register_href']);
@@ -92,4 +98,29 @@ class SectorCatalogueContentTest extends TestCase
$this->get('/sectores/'.$unpublished->slug)->assertNotFound();
$this->get('/sectores/'.$inactive->slug)->assertNotFound();
}
public function test_selecting_a_sector_keeps_opportunities_scoped_on_reload_and_return(): void
{
$a = $this->sector();$b = $this->sector();
foreach ([$a, $b] as $sector) {
$slug = 'qa-isolated-'.Str::lower(Str::random(12));
Opportunity::create(['sector_id'=>$sector->id, 'reference'=>$slug, 'title'=>$slug, 'slug'=>$slug,
'summary'=>'Synthetic scoped opportunity', 'status'=>'published', 'published_at'=>now()->subDay()]);
}
$index = $this->get('/sectores')->assertOk()->inertiaProps();
foreach ([$a, $b, $a] as $sector) {
$url = collect($index['sectors'])->firstWhere('id', $sector->id)['url'];
$first = $this->get($url)->assertOk()->inertiaProps();
$reload = $this->get($url)->assertOk()->inertiaProps();
$this->assertSame($first['sector'], $reload['sector']);
$this->assertSame($first['opportunities'], $reload['opportunities']);
$this->assertSame($sector->id, $first['sector']['id']);
$this->assertSame(Opportunity::where('sector_id',$sector->id)->pluck('slug')->all(), array_column($first['opportunities'],'slug'));
$this->assertSame('/investimentos/a-partir-do-setor/'.$sector->slug, $first['sectorEntry']['start_href']);
$this->get($first['sectorEntry']['start_href'])->assertRedirect('/login');
$returned = $this->get('/sectores')->assertOk()->inertiaProps();
$this->assertSame($index['sectors'], $returned['sectors']);
$this->assertArrayNotHasKey('sectorDetails', $returned);
}
}
}
@@ -0,0 +1,19 @@
{
"fixture_notice": "Synthetic public projection of accepted API QA consumers;no profiles,real pilots,credentials,dossier or personal data. Test input only;never import into application data.",
"consumer_contexts": [
{
"prefill": {
"simulator": {
"context_applicability_snapshot": "{\"version\":1,\"consumer_opportunity\":\"opportunity:399\",\"opportunity_choices\":[{\"opportunity\":{\"key\":\"opportunity:399\",\"title\":\"SYNTHETIC QA — Cahora Bassa\",\"version\":2,\"analysis\":{\"proactive\":{\"need\":\"SYNTHETIC QA necessidade por verificar.\",\"basis\":\"Fixture pública sintética; não é evidência de procura local.\",\"group\":\"ideas\",\"stage\":\"Apoio — correspondência real por confirmar\",\"state\":\"hypothesis\",\"notice\":\"A ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.\",\"version\":1,\"chain_id\":null,\"evidence\":[{\"id\":\"3587d2e4-7428-4cd7-994b-60607b20d7da\",\"scope\":\"Cahora Bassa — contexto piloto\",\"period\":\"Exercício2026 — cenário sintético\",\"source\":\"SYNTHETIC QA exemplar público fictício Cahora Bassa\",\"locator\":\"Fixture QA, linha1\",\"purpose\":\"local\",\"category\":\"infrastructure\",\"coverage\":\"Cobertura fictícia; informação real por confirmar\",\"statement\":\"SYNTHETIC QA: capacidade fictícia em Cahora Bassa; não é diagnóstico económico real.\",\"verified_on\":\"2026-10-27\"}],\"proposal\":\"SYNTHETIC QA solução em preparação.\",\"relations\":[],\"limitations\":\"Sem validação económica, contrato, terra ou energia garantidos.\",\"next_action\":\"Confirmar evidências reais antes de decidir.\",\"evidence_ids\":[\"3587d2e4-7428-4cd7-994b-60607b20d7da\"],\"economic_validation\":false,\"availability_confirmed\":false}}}}]}"
}
}
},
{
"prefill": {
"simulator": {
"context_applicability_snapshot": "{\"version\":1,\"consumer_opportunity\":\"opportunity:400\",\"opportunity_choices\":[{\"opportunity\":{\"key\":\"opportunity:400\",\"title\":\"SYNTHETIC QA — Gurué\",\"version\":2,\"analysis\":{\"proactive\":{\"need\":\"SYNTHETIC QA necessidade por verificar.\",\"basis\":\"Fixture pública sintética; não é evidência de procura local.\",\"group\":\"ideas\",\"stage\":\"Apoio — correspondência real por confirmar\",\"state\":\"hypothesis\",\"notice\":\"A ausência de registo não comprova ausência de oferta. Proximidade não comprova contrato, participação, energia acessível, terra disponível ou benefício fiscal. Necessidade confirmada não comprova viabilidade comercial; hipótese de fornecimento não é encomenda.\",\"version\":1,\"chain_id\":null,\"evidence\":[{\"id\":\"1421bb80-78c0-40c9-9de0-f45e18c5a093\",\"scope\":\"Gurué — contexto piloto\",\"period\":\"Exercício2026 — cenário sintético\",\"source\":\"SYNTHETIC QA exemplar público fictício Gurué\",\"locator\":\"Fixture QA, linha1\",\"purpose\":\"local\",\"category\":\"infrastructure\",\"coverage\":\"Cobertura fictícia; informação real por confirmar\",\"statement\":\"SYNTHETIC QA: capacidade fictícia em Gurué; não é diagnóstico económico real.\",\"verified_on\":\"2026-10-27\"}],\"proposal\":\"SYNTHETIC QA solução em preparação.\",\"relations\":[],\"limitations\":\"Sem validação económica, contrato, terra ou energia garantidos.\",\"next_action\":\"Confirmar evidências reais antes de decidir.\",\"evidence_ids\":[\"1421bb80-78c0-40c9-9de0-f45e18c5a093\"],\"economic_validation\":false,\"availability_confirmed\":false}}}}]}"
}
}
}
]
}
+1 -1
View File
@@ -6,7 +6,7 @@ test('absent proactivity is a no-op for accepted dossiers',()=>{assert.deepEqual
test('each idea retains origin,source,need,limits without summing or inventing economic values',()=>{const choices=[{opportunity:source(1,'A')},{opportunity:source(2,'B')}],d={context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:choices})},r=proactiveInvestmentRows(d);assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,2);assert.match(JSON.stringify(r),/opportunity:1/);assert.match(JSON.stringify(r),/opportunity:2/);assert.match(JSON.stringify(r),/Hipótese analítica/);assert.match(JSON.stringify(r),/não é zero/);assert.equal(d.context_applicability_snapshot,JSON.stringify({version:1,opportunity_choices:choices}));});
test('consumer selection only reports its idea even when other source contexts are present',()=>{const choices=[{opportunity:source(1,'A')},{opportunity:source(2,'B')}],r=proactiveInvestmentRows({context_opportunity:'opportunity:2',context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:choices})});assert.doesNotMatch(JSON.stringify(r),/opportunity:1/);assert.match(JSON.stringify(r),/opportunity:2/);});
test('explicit consumer snapshot is retained and repeat references do not duplicate report rows',()=>{const a=source(1,'A');let r=proactiveInvestmentRows({context_opportunity_snapshot:JSON.stringify({choice:{opportunity:a}})});assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,1);r=proactiveInvestmentRows({context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:[{opportunity:a},{opportunity:a}]})});assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,1);});
test('new API-recovered multi idea snapshot produces an independently scoped report',()=>{const d=JSON.parse(fs.readFileSync('outputs/proactive-investment-20261027/recovered-context.json','utf8'));const r=d.consumer_contexts.flatMap(c=>proactiveInvestmentRows(c.prefill.simulator));assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,2);assert.doesNotMatch(JSON.stringify(r),/PRIVATE-QA-/);fs.writeFileSync('outputs/proactive-investment-20261027/recovered-report-rows.json',JSON.stringify(r,null,2));});
test('portable API consumer snapshot produces an independently scoped report',()=>{const d=JSON.parse(fs.readFileSync(new URL('./fixtures/proactive-api-consumers.json',import.meta.url),'utf8'));const before=JSON.stringify(d);const perConsumer=d.consumer_contexts.map(c=>proactiveInvestmentRows(c.prefill.simulator));for(const rows of perConsumer)assert.equal(rows.filter(x=>x[1]==='Origem da análise e ideia').length,1);const r=perConsumer.flat();assert.equal(r.filter(x=>x[1]==='Origem da análise e ideia').length,2);assert.match(JSON.stringify(perConsumer[0]),/opportunity:399/);assert.doesNotMatch(JSON.stringify(perConsumer[0]),/opportunity:400/);assert.match(JSON.stringify(perConsumer[1]),/opportunity:400/);assert.doesNotMatch(JSON.stringify(perConsumer[1]),/opportunity:399/);assert.doesNotMatch(JSON.stringify(r),/PRIVATE-QA-/);assert.equal(JSON.stringify(d),before);});
test('resources,assumptions and selected relationships are retained in the existing dossier report',()=>{const a=source(7,'A');Object.assign(a.analysis.proactive,{resources:'QA recurso declarado',assumptions:'QA pressuposto explícito',conditions:'QA condição',relations:[{kind:'project',target_id:'QA-reference',label:'QA projecto existente',relationship:'hypothesis',basis:'QA fundamento público'}]});const draft={context_applicability_snapshot:JSON.stringify({version:1,opportunity_choices:[{opportunity:a}]})},rows=buildProjectDossierRows(draft);assert.match(JSON.stringify(rows),/QA pressuposto explícito/);assert.match(JSON.stringify(rows),/QA projecto existente/);assert.match(JSON.stringify(rows),/sem contrato, acesso ou participação presumidos/);});