diff --git a/app/Services/InvestmentCatalogue.php b/app/Services/InvestmentCatalogue.php
index d6a6e860..3bd96dbb 100644
--- a/app/Services/InvestmentCatalogue.php
+++ b/app/Services/InvestmentCatalogue.php
@@ -538,11 +538,12 @@ class InvestmentCatalogue
}
$public = app(ProactiveInvestment::class)->publicData($record);
$evidence = array_values(array_filter(StructuredData::collectionItems($public['evidence'] ?? []),
- fn ($proof) => in_array($proof['category'] ?? null, ['resource', 'infrastructure'], true) && ($proof['purpose'] ?? null) === 'local'));
+ fn ($proof) => in_array($proof['category'] ?? null, ['resource', 'infrastructure', 'workforce', 'activity', 'supply', 'demand'], true) && ($proof['purpose'] ?? null) === 'local'));
if ($evidence !== []) {
$thematic[] = ['id' => $record->id, 'title' => $record->title, 'url' => $properties['url'], 'location' => $properties['location'],
'state' => $public['state'] ?? 'hypothesis', 'limitations' => $public['limitations'] ?? null,
- 'evidence' => array_map(fn ($proof) => array_intersect_key($proof, array_flip(['id', 'category', 'statement', 'source', 'locator', 'period', 'scope', 'coverage', 'verified_on'])), $evidence)];
+ 'evidence' => array_map(fn ($proof) => [...array_intersect_key($proof, array_flip(['id', 'statement', 'source', 'locator', 'period', 'scope', 'coverage', 'verified_on'])),
+ 'source_category' => $proof['category'], 'category' => in_array($proof['category'], ['activity', 'supply', 'demand'], true) ? 'economic_potential' : $proof['category']], $evidence)];
}
$sites = collect($points->get($record->id) ?? []);
if ($sites->isNotEmpty()) {
diff --git a/app/Services/ProactiveInvestment.php b/app/Services/ProactiveInvestment.php
index 52cb6757..172522d7 100644
--- a/app/Services/ProactiveInvestment.php
+++ b/app/Services/ProactiveInvestment.php
@@ -33,7 +33,7 @@ final class ProactiveInvestment
'proactive.evidence.*.observation_id' => 'sometimes|nullable|string|size:64',
'proactive.supply_comparisons' => 'sometimes|array|max:40',
'proactive.evidence.*.id' => 'required|uuid|distinct',
- 'proactive.evidence.*.category' => 'required|in:activity,resource,infrastructure,supply,demand',
+ 'proactive.evidence.*.category' => 'required|in:activity,resource,infrastructure,supply,demand,workforce',
'proactive.evidence.*.purpose' => 'required|in:local,methodology',
'proactive.evidence.*.access' => 'required|in:internal,public',
'proactive.evidence.*.document_id' => 'nullable|uuid',
diff --git a/docs/investmoz/consolidated-review-matrix-2026-10-03.md b/docs/investmoz/consolidated-review-matrix-2026-10-03.md
index 04884677..f1403ad5 100644
--- a/docs/investmoz/consolidated-review-matrix-2026-10-03.md
+++ b/docs/investmoz/consolidated-review-matrix-2026-10-03.md
@@ -2243,3 +2243,18 @@ Delta aprovado: oito testes de BD/438 asserções (quatro novos e quatro de filt
Conferência visual no Edge exclusivamente em `127.0.0.2:18644`, base `main_visual_20261104_test`: duas fixtures claramente sintéticas criadas apenas em QA, uma por categoria; nenhuma coordenada/geometria criada. Hash de todas as oportunidades anteriores idêntico antes/depois. Alternância entre ambas conserva pesquisa provincial e duas fichas gerais; cada camada mostra a sua ficha, fonte/período/âmbito/limites e ausência de ponto. Infra-estruturas recuperada após recarregar. Recibo: `docs/investmoz/unification/thematic-map-local-20261104.json`; inventário, hashes, DOM e capturas em `outputs/thematic-map-20261104/`. Demonstração sintética não constitui cobertura económica factual ou publicação de dados reais.
**ACT-02 permanece parcial.** Cobertura factual de recursos/infra-estruturas no destino, georreferenciação própria quando documentada, mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e lacunas cartográficas continuam pendentes. Não se inferem estas camadas de descrições nem do número de oportunidades. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, alteração activa, migração, publicação automática ou alteração de permissões/serviços.
+
+
+### 04/11/2026 — ACT-20261029-02: mão-de-obra documentada e contexto económico
+
+Incremento qualitativo na main local sobre `21ebad73`, reutilizando mapa, evidências, síntese publicada, divulgação expressa e fontes/períodos existentes. Inventário na cópia QA: duas evidências públicas locais de actividade anteriores (fichas sintéticas 350/383), nenhuma de mão-de-obra/oferta/procura; recursos/infra-estruturas da demonstração anterior conservados. As declarações labour_demands/training_capacities têm acesso EducationAccess interno; emprego/indicadores financeiros canónicos não fazem parte da publicProjection. Observações de zonas conservam divulgação/unidades/metodologia próprias. Não foram expostas nem agregadas essas fontes privadas por existir revisão interna. As fontes oficiais aceites e as decisões de divulgação anteriores não foram reabertas.
+
+Acrescentada categoria explícita Mão-de-obra no editor e validação da evidência existente, sem migração ou reclassificação automática dos textos anteriores. Guardar, recuperar, pré-visualizar e confirmar publicação usam o percurso existente. A camada económica reutiliza apenas evidências públicas locais de actividade/oferta/procura, mantendo `source_category` original, facto, fonte, localizador, período, verificação, âmbito, cobertura e limitações. Agrupá-las para consulta é uma escolha de apresentação, não aprovação de viabilidade nem índice de potencial.
+
+Ambas as opções entram no selector do mapa único; a escolha é recuperada na sessão. Recursos/infra-estruturas, geometrias públicas, filtros territoriais, fichas sem ponto e contagens anteriores mantêm-se. Nenhuma posição é criada ou atribuída ao recurso/mão-de-obra. Necessidade ou qualificação não equivale a trabalhador disponível; previsão de emprego não é emprego efectivo. Contexto económico não calcula retorno, score ou viabilidade. Períodos/fontes incompatíveis permanecem separados, sem totais ou zero presumido.
+
+Delta aprovado: sete testes de BD/77 asserções (cinco do mapa, incluindo o novo caso; dois de publicação, um novo e um de acesso/fonte privada já existente), 22 testes frontend, PHPStan sem erros, tipos, Pint, lint e build QA aprovados. Teste do percurso: criar evidência workforce → recuperar → confirmar pela publicação existente → consultar mapa → alterar rascunho privado sem substituir a categoria/fonte públicas. Testes mantêm metodologia/internos fora do mapa, categorias originais, períodos distintos e ausência de quantidades/índices inferidos. Não se repetiu a bateria geral aceite.
+
+Conferência visual no Edge exclusivamente na QA `127.0.0.2:18644`/`main_visual_20261104_test`: contexto económico reutiliza a evidência de actividade já publicada da ficha 383, sintética e explicitamente identificada. Mão-de-obra usa uma única fixture nova, também sintética; escolha e fonte recuperadas após recarregar. Nenhuma coordenada/geometria criada; hash de todas as oportunidades anteriores idêntico antes/depois. Conferidos 36 artefactos dos três recibos anteriores, todos com SHA256 inalterado. Recibo: `docs/investmoz/unification/workforce-economic-map-local-20261104.json`; inventário, DOM, capturas e hashes em `outputs/workforce-economic-map-20261104/`.
+
+**ACT-02 permanece parcial.** Cobertura factual laboral/económica, séries, unidades e metodologia quantitativa comparável, georreferenciação própria documentada e cobertura cartográfica continuam pendentes. A demonstração sintética não mede oferta laboral nem potencial regional real. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, migração, publicação automática, escrita activa, alteração de permissões ou reinício de serviços.
diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md
index 2fdf80ab..5b8bfcab 100644
--- a/docs/investmoz/unification/traceability-and-manual.md
+++ b/docs/investmoz/unification/traceability-and-manual.md
@@ -9139,3 +9139,18 @@ Delta aprovado: oito testes de BD/438 asserções (quatro novos e quatro de filt
Conferência visual no Edge exclusivamente em `127.0.0.2:18644`, base `main_visual_20261104_test`: duas fixtures claramente sintéticas criadas apenas em QA, uma por categoria; nenhuma coordenada/geometria criada. Hash de todas as oportunidades anteriores idêntico antes/depois. Alternância entre ambas conserva pesquisa provincial e duas fichas gerais; cada camada mostra a sua ficha, fonte/período/âmbito/limites e ausência de ponto. Infra-estruturas recuperada após recarregar. Recibo: `docs/investmoz/unification/thematic-map-local-20261104.json`; inventário, hashes, DOM e capturas em `outputs/thematic-map-20261104/`. Demonstração sintética não constitui cobertura económica factual ou publicação de dados reais.
**ACT-02 permanece parcial.** Cobertura factual de recursos/infra-estruturas no destino, georreferenciação própria quando documentada, mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e lacunas cartográficas continuam pendentes. Não se inferem estas camadas de descrições nem do número de oportunidades. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, alteração activa, migração, publicação automática ou alteração de permissões/serviços.
+
+
+### 04/11/2026 — ACT-20261029-02: mão-de-obra documentada e contexto económico
+
+Incremento qualitativo na main local sobre `21ebad73`, reutilizando mapa, evidências, síntese publicada, divulgação expressa e fontes/períodos existentes. Inventário na cópia QA: duas evidências públicas locais de actividade anteriores (fichas sintéticas 350/383), nenhuma de mão-de-obra/oferta/procura; recursos/infra-estruturas da demonstração anterior conservados. As declarações labour_demands/training_capacities têm acesso EducationAccess interno; emprego/indicadores financeiros canónicos não fazem parte da publicProjection. Observações de zonas conservam divulgação/unidades/metodologia próprias. Não foram expostas nem agregadas essas fontes privadas por existir revisão interna. As fontes oficiais aceites e as decisões de divulgação anteriores não foram reabertas.
+
+Acrescentada categoria explícita Mão-de-obra no editor e validação da evidência existente, sem migração ou reclassificação automática dos textos anteriores. Guardar, recuperar, pré-visualizar e confirmar publicação usam o percurso existente. A camada económica reutiliza apenas evidências públicas locais de actividade/oferta/procura, mantendo `source_category` original, facto, fonte, localizador, período, verificação, âmbito, cobertura e limitações. Agrupá-las para consulta é uma escolha de apresentação, não aprovação de viabilidade nem índice de potencial.
+
+Ambas as opções entram no selector do mapa único; a escolha é recuperada na sessão. Recursos/infra-estruturas, geometrias públicas, filtros territoriais, fichas sem ponto e contagens anteriores mantêm-se. Nenhuma posição é criada ou atribuída ao recurso/mão-de-obra. Necessidade ou qualificação não equivale a trabalhador disponível; previsão de emprego não é emprego efectivo. Contexto económico não calcula retorno, score ou viabilidade. Períodos/fontes incompatíveis permanecem separados, sem totais ou zero presumido.
+
+Delta aprovado: sete testes de BD/77 asserções (cinco do mapa, incluindo o novo caso; dois de publicação, um novo e um de acesso/fonte privada já existente), 22 testes frontend, PHPStan sem erros, tipos, Pint, lint e build QA aprovados. Teste do percurso: criar evidência workforce → recuperar → confirmar pela publicação existente → consultar mapa → alterar rascunho privado sem substituir a categoria/fonte públicas. Testes mantêm metodologia/internos fora do mapa, categorias originais, períodos distintos e ausência de quantidades/índices inferidos. Não se repetiu a bateria geral aceite.
+
+Conferência visual no Edge exclusivamente na QA `127.0.0.2:18644`/`main_visual_20261104_test`: contexto económico reutiliza a evidência de actividade já publicada da ficha 383, sintética e explicitamente identificada. Mão-de-obra usa uma única fixture nova, também sintética; escolha e fonte recuperadas após recarregar. Nenhuma coordenada/geometria criada; hash de todas as oportunidades anteriores idêntico antes/depois. Conferidos 36 artefactos dos três recibos anteriores, todos com SHA256 inalterado. Recibo: `docs/investmoz/unification/workforce-economic-map-local-20261104.json`; inventário, DOM, capturas e hashes em `outputs/workforce-economic-map-20261104/`.
+
+**ACT-02 permanece parcial.** Cobertura factual laboral/económica, séries, unidades e metodologia quantitativa comparável, georreferenciação própria documentada e cobertura cartográfica continuam pendentes. A demonstração sintética não mede oferta laboral nem potencial regional real. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, migração, publicação automática, escrita activa, alteração de permissões ou reinício de serviços.
diff --git a/docs/investmoz/unification/workforce-economic-map-local-20261104.json b/docs/investmoz/unification/workforce-economic-map-local-20261104.json
new file mode 100644
index 00000000..ef1be3bb
--- /dev/null
+++ b/docs/investmoz/unification/workforce-economic-map-local-20261104.json
@@ -0,0 +1,168 @@
+{
+ "date": "2026-11-04",
+ "base_commit": "21ebad73",
+ "requirement": "ACT-20261029-02; V2-07/V2-10",
+ "scope": "Incremento qualitativo de mão-de-obra e contexto do potencial económico; main local",
+ "source_inventory": {
+ "database": "main_visual_20261104_test",
+ "public_local_evidence": {
+ "resource": 1,
+ "infrastructure": 1,
+ "workforce": 0,
+ "activity": 2,
+ "supply": 0,
+ "demand": 0
+ },
+ "private_sources_not_disclosed": [
+ "labour_demands/training_capacities: EducationAccess authorization",
+ "canonical employment and financial payloads: not part of publicProjection",
+ "zone observations: separate disclosure/units/methodology; not automatically mixed with opportunity evidence"
+ ],
+ "existing_economic_sources": [
+ {
+ "opportunity_id": 383,
+ "title": "SYNTHETIC QA — ideia de apoio em Cahora Bassa",
+ "category": "activity",
+ "source": "SYNTHETIC QA exemplar público da interface",
+ "period": "2026 — cenário sintético",
+ "scope": "Cahora Bassa — referência distrital existente"
+ },
+ {
+ "opportunity_id": 350,
+ "title": "SYNTHETIC QA — ideia de apoio em Cahora Bassa",
+ "category": "activity",
+ "source": "SYNTHETIC QA exemplar público da interface",
+ "period": "2026 — cenário sintético",
+ "scope": "Cahora Bassa — referência distrital existente"
+ }
+ ]
+ },
+ "implementation": [
+ "Categoria workforce no editor/validação existentes; mesmos campos de fonte, período, âmbito e divulgação expressa.",
+ "Mapeamento de activity/supply/demand já públicos para a camada economic_potential, conservando source_category e cada fonte/período.",
+ "Apenas evidência publicada, seleccionada e de propósito local; rascunhos privados e metodologia excluídos.",
+ "Escolha de ambas as novas camadas recuperável na sessão; pontos/áreas limitados aos locais existentes das oportunidades.",
+ "Sem unidades, quantidades, disponibilidade laboral, índices ou retorno calculados a partir dos textos; sem agregação de períodos/fontes incompatíveis."
+ ],
+ "existing_private_sources": [
+ "EducationAccess protege labour_demands/training_capacities; revisão interna não é publicação.",
+ "OpportunityCanonicalContract::publicProjection não divulga emprego/indicadores financeiros privados.",
+ "Observações de zonas conservam divulgação, unidades e metodologia próprias; não foram automaticamente misturadas."
+ ],
+ "contract": {
+ "route": "GET /api/v1/investment/map",
+ "field": "thematic_records.evidence",
+ "new_categories": [
+ "workforce",
+ "economic_potential"
+ ],
+ "source_category": "categoria original explícita; recursos/infra-estruturas mantêm categoria anterior",
+ "version_compatibility": "Campos/fichas/históricos anteriores conservados; adição de categoria no rascunho existente, sem migração"
+ },
+ "checks": {
+ "database_tests": 7,
+ "database_assertions": 77,
+ "map_tests": 5,
+ "publication_tests": 2,
+ "frontend_tests": 22,
+ "phpstan_errors": 0,
+ "types": "passed",
+ "pint": "passed",
+ "lint": "passed",
+ "qa_build": "passed; avisos de fonte opcional e dimensão de chunks"
+ },
+ "visual": {
+ "url_base": "http://127.0.0.2:18644",
+ "database": "main_visual_20261104_test",
+ "economic_existing_opportunity_id": 383,
+ "economic_source_reused": "Evidência de actividade já pública, explicitamente sintética no snapshot QA; original preservado.",
+ "new_workforce_fixture_ids": [
+ 534
+ ],
+ "workforce_recovered_after_reload": true,
+ "new_coordinates": false,
+ "new_geometry": false,
+ "existing_opportunities_sha256": "90aaee1f9e47bb7793e18746fb1f6935bfb0a21329deca9c569707fadd2c04c6"
+ },
+ "preservation": {
+ "prior_artifact_hashes_verified_unchanged": 36,
+ "accepted_full_database_tests_preserved_not_reexecuted": 1325,
+ "previous_pending_items_retained": true
+ },
+ "pending": [
+ "Cobertura factual laboral e económica real, séries comparáveis, unidades e metodologia quantitativa permanecem pendentes.",
+ "A camada económica é contexto documental, sem classificação de viabilidade ou score regional.",
+ "Procura de trabalho, formação, empregos previstos e trabalhadores disponíveis não são intercambiáveis.",
+ "Conferência remota, aceitação da equipa e cobertura cartográfica permanecem pendentes; ACT-02/V2-07/V2-10 não encerrados."
+ ],
+ "safety": {
+ "push": false,
+ "active_database_changed": false,
+ "migration": false,
+ "permissions_changed": false,
+ "automatic_publication": false,
+ "services_restarted": false
+ },
+ "proofs": {
+ "outputs/workforce-economic-map-20261104/source-inventory-before-fixtures.json": {
+ "sha256": "cd8a8943bf86778e041cc3f2ced51467d44e6e83147c8a8215024a27431d2964",
+ "bytes": 1313
+ },
+ "outputs/workforce-economic-map-20261104/fixture-receipt.json": {
+ "sha256": "7d680fed74a70d4367f1a7d8e0af60a0b7c5e5f89bc3ff4a17be5e119d294dc0",
+ "bytes": 405
+ },
+ "outputs/workforce-economic-map-20261104/existing-economic-context.jpg": {
+ "sha256": "6f81b29cbefad611b035fe26324d4b876d5dd5a9499a6fbd8ee75e8b4416aec5",
+ "bytes": 157445
+ },
+ "outputs/workforce-economic-map-20261104/existing-economic-context.dom.txt": {
+ "sha256": "1eb2687e9a3e73f4b3ed9808a9328930c54ca08dd6270e7d2e7bff1cfbef7227",
+ "bytes": 20535
+ },
+ "outputs/workforce-economic-map-20261104/workforce-reopened.jpg": {
+ "sha256": "b4c919ca4bfc344cd4feb5adcbe4a483d241d549d75450ee62a8b3854ccc6f34",
+ "bytes": 161166
+ },
+ "outputs/workforce-economic-map-20261104/workforce-reopened.dom.txt": {
+ "sha256": "01c6081409ea36d88e01d1c8b9af558eb497e68b596fe8be1b45cdec437c7515",
+ "bytes": 13566
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-map.log": {
+ "sha256": "c6fb7cddb76a2589f3c41dc3d144b0387573417ddd9afcff96f331b801f53c0f",
+ "bytes": 94
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-map.junit.xml": {
+ "sha256": "5824abb4aaa07c37e9420802d82a8ed08c4cd1babc76c337043016eb098f37ec",
+ "bytes": 2374
+ },
+ "outputs/main-db-qa-20261104/workforce-publication.log": {
+ "sha256": "3b0cb219fb68177a1a026ac7c076fe0da81f40c20aa6a8415a54866a08895c9d",
+ "bytes": 95
+ },
+ "outputs/main-db-qa-20261104/workforce-publication.junit.xml": {
+ "sha256": "7ad274166f6520990241a82530c51ac662540907bcc831c83889190176a80eaf",
+ "bytes": 958
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-frontend.log": {
+ "sha256": "02dc96a1acd6887ac3fdc282562e8e5579e05bcf29283330effdf5689619ee09",
+ "bytes": 2811
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-phpstan.log": {
+ "sha256": "1997495ca8f04c2ec09798d55607d1a2f85e3338b772f258666465bf658032d2",
+ "bytes": 49
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-types.log": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "bytes": 0
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-lint.log": {
+ "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
+ "bytes": 0
+ },
+ "outputs/main-db-qa-20261104/workforce-economic-build.log": {
+ "sha256": "ec0f15282f2dfe3c3c8dbe349773a88adbd87efba7e26332cd55c34b8f21bf25",
+ "bytes": 31606
+ }
+ }
+}
diff --git a/docs/investmoz/v2/auditoria-portal/backlog.md b/docs/investmoz/v2/auditoria-portal/backlog.md
index 05d8b0c9..eda0d353 100644
--- a/docs/investmoz/v2/auditoria-portal/backlog.md
+++ b/docs/investmoz/v2/auditoria-portal/backlog.md
@@ -3447,3 +3447,18 @@ Delta aprovado: oito testes de BD/438 asserções (quatro novos e quatro de filt
Conferência visual no Edge exclusivamente em `127.0.0.2:18644`, base `main_visual_20261104_test`: duas fixtures claramente sintéticas criadas apenas em QA, uma por categoria; nenhuma coordenada/geometria criada. Hash de todas as oportunidades anteriores idêntico antes/depois. Alternância entre ambas conserva pesquisa provincial e duas fichas gerais; cada camada mostra a sua ficha, fonte/período/âmbito/limites e ausência de ponto. Infra-estruturas recuperada após recarregar. Recibo: `docs/investmoz/unification/thematic-map-local-20261104.json`; inventário, hashes, DOM e capturas em `outputs/thematic-map-20261104/`. Demonstração sintética não constitui cobertura económica factual ou publicação de dados reais.
**ACT-02 permanece parcial.** Cobertura factual de recursos/infra-estruturas no destino, georreferenciação própria quando documentada, mão-de-obra, potencial económico quantitativo, restantes fontes temáticas e lacunas cartográficas continuam pendentes. Não se inferem estas camadas de descrições nem do número de oportunidades. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, alteração activa, migração, publicação automática ou alteração de permissões/serviços.
+
+
+### 04/11/2026 — ACT-20261029-02: mão-de-obra documentada e contexto económico
+
+Incremento qualitativo na main local sobre `21ebad73`, reutilizando mapa, evidências, síntese publicada, divulgação expressa e fontes/períodos existentes. Inventário na cópia QA: duas evidências públicas locais de actividade anteriores (fichas sintéticas 350/383), nenhuma de mão-de-obra/oferta/procura; recursos/infra-estruturas da demonstração anterior conservados. As declarações labour_demands/training_capacities têm acesso EducationAccess interno; emprego/indicadores financeiros canónicos não fazem parte da publicProjection. Observações de zonas conservam divulgação/unidades/metodologia próprias. Não foram expostas nem agregadas essas fontes privadas por existir revisão interna. As fontes oficiais aceites e as decisões de divulgação anteriores não foram reabertas.
+
+Acrescentada categoria explícita Mão-de-obra no editor e validação da evidência existente, sem migração ou reclassificação automática dos textos anteriores. Guardar, recuperar, pré-visualizar e confirmar publicação usam o percurso existente. A camada económica reutiliza apenas evidências públicas locais de actividade/oferta/procura, mantendo `source_category` original, facto, fonte, localizador, período, verificação, âmbito, cobertura e limitações. Agrupá-las para consulta é uma escolha de apresentação, não aprovação de viabilidade nem índice de potencial.
+
+Ambas as opções entram no selector do mapa único; a escolha é recuperada na sessão. Recursos/infra-estruturas, geometrias públicas, filtros territoriais, fichas sem ponto e contagens anteriores mantêm-se. Nenhuma posição é criada ou atribuída ao recurso/mão-de-obra. Necessidade ou qualificação não equivale a trabalhador disponível; previsão de emprego não é emprego efectivo. Contexto económico não calcula retorno, score ou viabilidade. Períodos/fontes incompatíveis permanecem separados, sem totais ou zero presumido.
+
+Delta aprovado: sete testes de BD/77 asserções (cinco do mapa, incluindo o novo caso; dois de publicação, um novo e um de acesso/fonte privada já existente), 22 testes frontend, PHPStan sem erros, tipos, Pint, lint e build QA aprovados. Teste do percurso: criar evidência workforce → recuperar → confirmar pela publicação existente → consultar mapa → alterar rascunho privado sem substituir a categoria/fonte públicas. Testes mantêm metodologia/internos fora do mapa, categorias originais, períodos distintos e ausência de quantidades/índices inferidos. Não se repetiu a bateria geral aceite.
+
+Conferência visual no Edge exclusivamente na QA `127.0.0.2:18644`/`main_visual_20261104_test`: contexto económico reutiliza a evidência de actividade já publicada da ficha 383, sintética e explicitamente identificada. Mão-de-obra usa uma única fixture nova, também sintética; escolha e fonte recuperadas após recarregar. Nenhuma coordenada/geometria criada; hash de todas as oportunidades anteriores idêntico antes/depois. Conferidos 36 artefactos dos três recibos anteriores, todos com SHA256 inalterado. Recibo: `docs/investmoz/unification/workforce-economic-map-local-20261104.json`; inventário, DOM, capturas e hashes em `outputs/workforce-economic-map-20261104/`.
+
+**ACT-02 permanece parcial.** Cobertura factual laboral/económica, séries, unidades e metodologia quantitativa comparável, georreferenciação própria documentada e cobertura cartográfica continuam pendentes. A demonstração sintética não mede oferta laboral nem potencial regional real. Mantêm-se conferência remota e aceitação da equipa; V2-07/V2-10 não encerrados. Sem push, migração, publicação automática, escrita activa, alteração de permissões ou reinício de serviços.
diff --git a/resources/js/components/proactive-investment.tsx b/resources/js/components/proactive-investment.tsx
index 5bfdb46a..80f65e68 100644
--- a/resources/js/components/proactive-investment.tsx
+++ b/resources/js/components/proactive-investment.tsx
@@ -399,6 +399,8 @@ export function ProactiveInvestmentEditor({
infrastructure: 'Infra-estrutura',
supply: 'Oferta',
demand: 'Procura',
+ workforce:
+ 'Mão-de-obra — evidência documentada',
},
(v) => change({ category: v }),
)}
diff --git a/resources/js/components/thematic-investment-map.tsx b/resources/js/components/thematic-investment-map.tsx
index 568b85bd..de1d5819 100644
--- a/resources/js/components/thematic-investment-map.tsx
+++ b/resources/js/components/thematic-investment-map.tsx
@@ -14,7 +14,12 @@ export type ThematicMapRecord = {
limitations: string | null;
evidence: {
id: string;
- category: 'resource' | 'infrastructure';
+ category:
+ | 'resource'
+ | 'infrastructure'
+ | 'workforce'
+ | 'economic_potential';
+ source_category?: string;
statement?: string | null;
source?: string | null;
locator?: string | null;
@@ -27,6 +32,16 @@ export type ThematicMapRecord = {
const labels = {
resource: 'Recursos documentados',
infrastructure: 'Infra-estruturas documentadas',
+ workforce: 'Mão-de-obra documentada',
+ economic_potential: 'Potencial económico — contexto documentado',
+};
+const sourceLabels: Record
- Mão-de-obra e potencial económico: sem camada quantitativa - nesta consulta; não são inferidos de descrições ou do número - de oportunidades. + Mão-de-obra: a evidência pode descrever necessidades ou + qualificações; não mede trabalhadores disponíveis. Potencial + económico: contexto de actividade, oferta e procura, sem + índice, previsão de retorno ou viabilidade calculados. + Fontes e períodos não são agregados como se fossem + comparáveis.
{truncated && (@@ -152,13 +169,14 @@ export default function ThematicInvestmentMap({ ).length }{' '} sem ponto público utilizável. Ausência de evidência - não significa ausência do recurso. + não significa ausência de recursos ou actividade + económica.
{selected.length === 0 && (- Sem evidência pública local de recursos ou - infra-estruturas para esta camada e estes - filtros. Não foram criadas posições no mapa. + Sem evidência pública local para esta camada e + estes filtros. Não foram criadas posições no + mapa.
)} {selected.length > 0 && ( @@ -207,6 +225,15 @@ export default function ThematicInvestmentMap({{labels[proof.category]}
+ {proof.source_category && ( ++ Categoria de origem:{' '} + {sourceLabels[ + proof + .source_category + ] ?? 'Não indicada'} +
+ )}{value(proof.statement)}
diff --git a/tests/Feature/InvestmentThematicMapTest.php b/tests/Feature/InvestmentThematicMapTest.php index 735bd54b..3d9a195a 100644 --- a/tests/Feature/InvestmentThematicMapTest.php +++ b/tests/Feature/InvestmentThematicMapTest.php @@ -55,15 +55,34 @@ class InvestmentThematicMapTest extends TestCase $record = $this->record($prefix, [$this->proof(), $this->proof('infrastructure'), $this->proof('resource', 'methodology'), $this->proof('demand')]); $before = $record->fresh()->getRawOriginal('workflow_context'); $response = $this->getJson($this->url($prefix))->assertOk()->assertJsonPath('matched_count', 1) - ->assertJsonPath('thematic_records.0.id', $record->id)->assertJsonCount(2, 'thematic_records.0.evidence') + ->assertJsonPath('thematic_records.0.id', $record->id)->assertJsonCount(3, 'thematic_records.0.evidence') ->assertJsonPath('thematic_records.0.evidence.0.source', 'Synthetic public source') ->assertJsonPath('thematic_records.0.evidence.0.period', '2026') ->assertJsonPath('thematic_records.0.evidence.0.scope', 'Synthetic district') + ->assertJsonPath('thematic_records.0.evidence.2.category', 'economic_potential') + ->assertJsonPath('thematic_records.0.evidence.2.source_category', 'demand') ->assertJsonPath('thematic_records.0.state', 'hypothesis')->assertJsonCount(0, 'features')->assertJsonCount(1, 'unlocated'); $this->assertStringNotContainsString('PRIVATE-DRAFT-MUST-NOT-LEAK', $response->getContent()); $this->assertSame($before, $record->fresh()->getRawOriginal('workflow_context')); } + public function test_workforce_is_explicit_and_economic_context_preserves_each_source_without_inferred_totals(): void + { + $prefix = 'THEME-'.Str::uuid(); + $this->record($prefix, [$this->proof('workforce'), $this->proof('activity'), $this->proof('supply'), $this->proof('demand'), $this->proof('workforce', 'methodology')]); + $r = $this->getJson($this->url($prefix))->assertOk()->assertJsonCount(4, 'thematic_records.0.evidence') + ->assertJsonPath('thematic_records.0.evidence.0.category', 'workforce') + ->assertJsonPath('thematic_records.0.evidence.0.source_category', 'workforce') + ->assertJsonPath('thematic_records.0.state', 'hypothesis')->assertJsonCount(0, 'features')->json('thematic_records.0'); + foreach (['activity', 'supply', 'demand'] as $i => $source) { + $this->assertSame('economic_potential', $r['evidence'][$i + 1]['category']); + $this->assertSame($source, $r['evidence'][$i + 1]['source_category']); + $this->assertSame('2026', $r['evidence'][$i + 1]['period']); + } + $this->assertArrayNotHasKey('available_workers', $r); + $this->assertArrayNotHasKey('potential_score', $r); + } + public function test_unpublished_future_restricted_and_unselected_private_evidence_are_not_disclosed(): void { $prefix = 'THEME-'.Str::uuid(); diff --git a/tests/Feature/ProactiveInvestmentTest.php b/tests/Feature/ProactiveInvestmentTest.php index ea9afd88..a6521d70 100644 --- a/tests/Feature/ProactiveInvestmentTest.php +++ b/tests/Feature/ProactiveInvestmentTest.php @@ -62,6 +62,26 @@ final class ProactiveInvestmentTest extends TestCase return $input; } + public function test_workforce_evidence_saves_recovers_and_requires_existing_explicit_publication(): void + { + $input = F::input(); + $input['proactive']['evidence'][0]['category'] = 'workforce'; + $id = $this->postJson('/minhas-oportunidades/drafts', $input)->assertCreated()->json('id'); + $o = Opportunity::findOrFail($id); + $analysis = app(OpportunityManagement::class)->values($o)['proactive']; + $this->assertSame('workforce', $analysis['evidence'][0]['category']); + $this->assertNull(app(ProactiveInvestment::class)->publicData($o)); + $this->publish($o); + $this->getJson('/api/v1/investment/map?opportunity_id='.$id)->assertOk() + ->assertJsonPath('thematic_records.0.evidence.0.category', 'workforce') + ->assertJsonPath('thematic_records.0.evidence.0.source', $analysis['evidence'][0]['source']) + ->assertJsonCount(1, 'thematic_records.0.evidence')->assertJsonCount(0, 'features'); + $analysis['evidence'][0]['category'] = 'demand'; + $this->putJson('/minhas-oportunidades/'.$id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o->fresh()), 'proactive' => $analysis, + 'reason' => 'SYNTHETIC QA later private changes must not replace the published workforce source'])->assertOk(); + $this->getJson('/api/v1/investment/map?opportunity_id='.$id)->assertOk()->assertJsonPath('thematic_records.0.evidence.0.category', 'workforce'); + } + public function test_legacy_records_without_analysis_add_no_publication_lookup(): void { $o = new Opportunity; diff --git a/tests/frontend/thematic-investment-map.test.mjs b/tests/frontend/thematic-investment-map.test.mjs index 481b5fd0..d58ebcf1 100644 --- a/tests/frontend/thematic-investment-map.test.mjs +++ b/tests/frontend/thematic-investment-map.test.mjs @@ -137,7 +137,7 @@ test('default preserves every opportunity and one map, with sourced thematic evi 'Partial coverage', '2026-10-01', 'não representam a posição exacta', - 'não são inferidos', + 'sem índice', ]) assert.ok(html.includes(text), text); assert.match(html, /2 fichas com evidência temática pública/); @@ -216,7 +216,7 @@ test('changing query data never retains previous evidence; loading and incomplet ); assert.match(html, /Sem evidência pública local/); assert.match(html, /A resposta está limitada/); - assert.match(html, /não significa ausência do recurso/); + assert.match(html, /não significa ausência de recursos/); }); test('theme selection survives reload in the tab and unavailable or invalid storage does not break the map', () => { @@ -276,3 +276,63 @@ test('theme selection survives reload in the tab and unavailable or invalid stor else globalThis.window = previous; } }); + +test('workforce and economic context preserve source categories and incompatible periods without inferred totals or borrowed points', () => { + const Component = setup(); + const extra = [ + { + ...records[0], + id: 4, + title: 'Workforce evidence', + evidence: [{ ...proof('workforce'), source_category: 'workforce' }], + }, + { + ...records[1], + id: 5, + title: 'Economic context', + evidence: [ + { + ...proof('economic_potential'), + id: 'supply', + source_category: 'supply', + period: '2023', + }, + { + ...proof('economic_potential'), + id: 'demand', + source_category: 'demand', + period: '2026', + }, + ], + }, + ]; + const data = { ...props, records: [...records, ...extra] }; + flat(Component(data)) + .find((node) => node.type === 'select') + .props.onChange({ target: { value: 'workforce' } }); + let tree = Component(data); + assert.deepEqual( + flat(tree).find((node) => node.type === Map).props.points, + [], + ); + let html = renderToStaticMarkup(tree); + assert.match(html, /Workforce evidence/); + assert.match(html, /Categoria de origem: Mão-de-obra/); + assert.match(html, /não mede trabalhadores disponíveis/); + flat(tree) + .find((node) => node.type === 'select') + .props.onChange({ target: { value: 'economic_potential' } }); + tree = Component(data); + html = renderToStaticMarkup(tree); + assert.match(html, /Economic context/); + assert.match(html, /Categoria de origem: Oferta/); + assert.match(html, /Categoria de origem: Procura/); + assert.match(html, /2023/); + assert.match(html, /2026/); + assert.match(html, /sem índice, previsão de retorno ou viabilidade/); + assert.doesNotMatch(html, /Workforce evidence/); + assert.deepEqual( + flat(tree).find((node) => node.type === Map).props.points, + [], + ); +});