Compare sourced territorial observations without inferring statistical equivalence
tests / ci (push) Waiting to run

This commit is contained in:
2026-11-13 21:52:38 +02:00
4 changed files with 345 additions and 3 deletions
@@ -9794,3 +9794,27 @@ Prova integrada: consulta das 19 etapas e 38 descargas reais pelo Edge (19 Word/
Validação nova, limitada ao delta: dois testes frontend (incluem exportação das 19 etapas e escape das limitações), tipos, lint dos três ficheiros de produto afectados e compilação QA. Não foram repetidas as provas aceites de APIEX/SWOT, cálculo fiscal, permissões ou instalação. O perfil antigo de QA referia contas que já não existem no cluster actualmente ligado; foi localizado e usado o perfil documentado da demografia, sem criar permissões ou reiniciar o cluster. A serialização da chave MFA da conta recém-criada foi corrigida apenas nessa conta sintética.
Estado: correcção implementada e conferida em QA, ainda sem instalação. A homologação mantém `07182c7c` para o recorte de relatórios anteriormente instalado. A prova demonstra resumos de preparação por etapa; não declara produzidos estudos financeiros, licenças, decisões, pacotes APIEX ou documentos que não existam nos respectivos percursos. A instalação deste novo delta é a pendência de entrega; os saldos próprios dos outros módulos mantêm-se.
## 13/11/2026 — resumos por etapa instalados na homologação em 8854b292
Autorização expressa do proprietário recebida e executada. Revisão servida `8854b2928c6907b6742a1eb622ae70e6b6a9e998`, construída em 2026-11-13T13:16:06Z; troca concluída em 13:16:09Z. Entrada habitual: `https://teste-investidor.tempo.co.mz/investimentos`, abrir o dossier e a secção de relatórios, seleccionar a etapa, consultar o resumo, preparar Word ou HTML e descarregar o ficheiro pronto. O relatório consolidado permanece disponível.
Backup/restauro fresco: 193 tabelas/409 ficheiros, conferidos por contagens e hashes. Verificação pós-entrega: quatro ficheiros por SHA256; 18 tabelas protegidas inalteradas, incluindo instituições, dossiers, rascunhos, documentos, jornadas, etapas e as três tabelas demográficas. Mantidos 12 lotes aprovados/920456 observações. Sem migrações, importações, publicação ou alterações de permissões. Preparação demográfica e os controlos autenticados foram preservados e a readiness foi vinculada à revisão instalada, mantendo o exemplar anterior arquivado.
O executor Git existente e os seus controlos obrigatórios de CI foram reutilizados sem alteração de fonte ou da configuração partilhada. A entrega ficou fixada na branch aprovada `entrega/relatorios-etapas-20261113`, pois a main já continha alterações alheias de pesquisa/suporte com integração falhada. Essas alterações e a main não foram substituídas nem instaladas por esta autorização.
Prova pós-instalação no Edge, na sessão autorizada já existente: dossier sintético `db85f087-0737-4164-892a-4fa4fe1313df`, resumo S01 e fundamento guardado recuperados; Word e HTML efectivamente descarregados e conferidos. Nenhum dossier foi alterado nesta conferência. Não se repetiram as 19 consultas/38 descargas aceites em QA; o CI obrigatório do executor correu normalmente. Recibo: `outputs/dossier-reports-20261113/stage-homologation-installed.json`; captura `visual/06-homologacao-S01-descarga.jpg` no mesmo directório.
Estado deste delta: instalado e conferido. Substitui a pendência de instalação do registo anterior, preservando os seus limites: são resumos de preparação dos registos disponíveis; não se declaram produzidas licenças, decisões, estudos ou outros originais ainda em falta nos respectivos percursos. O pacote QA selado `outputs/entrega-relatorios-etapas-20261113.zip` permanece inalterado; o recibo posterior comprova a instalação.
## 13/11/2026 — ACT-02: confronto de observações quantitativas, QA
Primeiro recorte executável da continuidade documentada de ACT-20261029-02: a comparação das observações públicas já recebidas no mapa. Reutiliza `InvestmentMapIndicators` e os filtros existentes; não acrescenta API, cadastro, publicação, fonte externa ou acesso a dados privados. Duas selecções progressivas mostram território, descrição/natureza da grandeza, indicador, valor textual exacto, unidade, período, âmbito, população/base, metodologia, situação, fonte/data e versão.
A conferência assinala diferenças de território/indicador/unidade/âmbito/base/metodologia, campos em falta (incluindo descrição/natureza), datas inválidas, escolha repetida e informação desconhecida/em divergência. Metadados alinhados não são certificação de comparabilidade. Duração, sobreposição dos períodos e natureza prevista/efectiva exigem leitura da fonte. Não soma, calcula evolução, converte grandezas, cria pontuação ou índice regional. Zero, precisão decimal e fontes originais são preservados.
Selecções identificadas por território, observação e versão. Fora dos filtros deixam de entrar na comparação; mudar a versão exige nova selecção. Conferência no Edge com três observações inteiramente sintéticas, sem API/escritas na base, exercitando o componente real compilado. Capturas e recibo em `outputs/map-comparison-20261113/`. Três testes novos dirigidos ao delta, tipos, lint e compilação QA aprovados; os testes aceites dos filtros anteriores não foram repetidos.
Estado: implementado e conferido em QA, sem instalação. V2-07/V2-10 e ACT-02 permanecem parciais: cobertura factual nacional/regional, séries estatisticamente comparáveis, oferta laboral, natureza estruturada das grandezas e metodologia de potencial económico continuam com o seu saldo próprio. Este incremento encerra apenas a ferramenta de confronto das observações recebidas. Não altera dossiers, relatórios de 8854b292, pesquisa ou suporte aceites. A candidata incorpora também o recibo documental de 8854b292, sem repetir a instalação ou as provas dessa entrega.
File diff suppressed because one or more lines are too long
@@ -55,6 +55,238 @@ const completeDate = (value?: string | null) =>
new Date(value).toISOString().slice(0, 10) === value,
);
type MapObservation = QuantitativeMapContext['records'][number];
const observationKey = (row: MapObservation) =>
JSON.stringify([row.zone_id, row.id, row.zone_version]);
export function compareMapObservations(
first: MapObservation,
second: MapObservation,
) {
const issues: string[] = [];
if (observationKey(first) === observationKey(second))
issues.push('Escolha duas observações distintas.');
if (first.zone_id !== second.zone_id)
issues.push(
'Territórios distintos: os valores não representam uma série do mesmo local.',
);
for (const [label, row] of [
['Primeira', first],
['Segunda', second],
] as const) {
if (row.knowledge !== 'documented')
issues.push(
`${label} observação: informação desconhecida ou em divergência.`,
);
if (
row.value === null ||
!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i.test(row.value)
)
issues.push(
`${label} observação: valor quantitativo por confirmar.`,
);
const comparisonMetadata: Record<string, string> = {
...metadataLabels,
description: 'descrição/natureza da grandeza',
};
const missing = Object.keys(comparisonMetadata).filter(
(field) => !row[field as keyof MapObservation]?.toString().trim(),
);
if (missing.length)
issues.push(
`${label} observação sem ${missing.map((field) => comparisonMetadata[field]).join(', ')}.`,
);
if (
!completeDate(row.period_start) ||
!completeDate(row.period_end) ||
(row.period_start ?? '') > (row.period_end ?? '')
)
issues.push(`${label} observação: período incompleto ou inválido.`);
if (!completeDate(row.source_date))
issues.push(
`${label} observação: data da fonte incompleta ou inválida.`,
);
}
for (const [field, label] of Object.entries({
indicator: 'Indicador',
unit: 'Unidade',
scope: 'Âmbito',
population: 'População/base',
methodology: 'Metodologia',
})) {
if (
(first[field as keyof MapObservation] ?? '').toString().trim() !==
(second[field as keyof MapObservation] ?? '').toString().trim()
)
issues.push(`${label} diferente entre as observações.`);
}
return {
state: issues.length ? 'review_required' : 'metadata_aligned',
issues,
};
}
export function MapObservationComparison({
first,
second,
}: {
first?: MapObservation;
second?: MapObservation;
}) {
if (!first || !second)
return (
<p role="status">
Seleccione duas observações disponíveis nos filtros actuais.
</p>
);
const assessment = compareMapObservations(first, second);
const fields: [string, (row: MapObservation) => string][] = [
['Território', (row) => row.zone_name],
['Observação', (row) => text(row.label)],
['Descrição e natureza da grandeza', (row) => text(row.description)],
['Indicador', (row) => labels[row.indicator] ?? row.indicator],
[
'Valor original',
(row) => (row.value === null ? 'Por confirmar' : row.value),
],
['Unidade', (row) => text(row.unit)],
[
'Período',
(row) => `${text(row.period_start)} — ${text(row.period_end)}`,
],
['Âmbito', (row) => text(row.scope)],
['População/base', (row) => text(row.population)],
['Metodologia', (row) => text(row.methodology)],
[
'Situação da informação',
(row) =>
row.knowledge === 'documented'
? 'Documentada'
: row.knowledge === 'disputed'
? 'Em divergência'
: 'Por confirmar',
],
['Fonte', (row) => text(row.source_reference)],
['Data da fonte', (row) => text(row.source_date)],
['Versão da ficha territorial', (row) => String(row.zone_version)],
];
return (
<div className="space-y-3">
<p role="status">
{assessment.state === 'metadata_aligned'
? 'Metadados alinhados; confirme nas fontes se os valores podem ser comparados.'
: 'Há diferenças ou lacunas a conferir antes de comparar os valores.'}
</p>
{!!assessment.issues.length && (
<ul className="list-disc pl-5">
{assessment.issues.map((issue) => (
<li key={issue}>{issue}</li>
))}
</ul>
)}
<p>
Períodos distintos ou sobrepostos exigem conferência da duração
e da base na fonte. Esta apresentação não calcula evolução,
soma, conversão, pontuação ou estimativa regional. A natureza
prevista ou efectiva depende da descrição da fonte.
</p>
<div className="overflow-x-auto">
<table className="w-full text-left text-sm">
<caption className="sr-only">
Conferência de duas observações quantitativas
</caption>
<thead>
<tr>
<th className="p-2">Campo</th>
<th className="p-2">Primeira observação</th>
<th className="p-2">Segunda observação</th>
</tr>
</thead>
<tbody>
{fields.map(([label, value]) => (
<tr key={label} className="border-t">
<th className="p-2" scope="row">
{label}
</th>
<td className="p-2 break-words">
{value(first)}
</td>
<td className="p-2 break-words">
{value(second)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
function MapObservationComparisonPicker({ rows }: { rows: MapObservation[] }) {
const id = useId();
const [firstKey, setFirst] = useState('');
const [secondKey, setSecond] = useState('');
const first = rows.find((row) => observationKey(row) === firstKey);
const second = rows.find((row) => observationKey(row) === secondKey);
return (
<details className="space-y-3 rounded border p-3">
<summary>Comparar duas observações</summary>
<p>
Confronte os dados e as fontes antes de os usar na decisão.
Selecções fora dos filtros deixam de ser comparadas; uma versão
nova exige nova selecção.
</p>
<div className="grid gap-3 sm:grid-cols-2">
{(['Primeira', 'Segunda'] as const).map((label, index) => (
<label
className="grid gap-1"
key={label}
htmlFor={`${id}-${index}`}
>
{label} observação
<select
className="w-full min-w-0 rounded border p-2"
id={`${id}-${index}`}
value={
index === 0
? first
? firstKey
: ''
: second
? secondKey
: ''
}
onChange={(event) =>
(index === 0 ? setFirst : setSecond)(
event.target.value,
)
}
>
<option value="">Seleccionar observação</option>
{rows.map((row) => (
<option
key={observationKey(row)}
value={observationKey(row)}
>
{row.zone_name} —{' '}
{row.label ||
labels[row.indicator] ||
row.indicator}{' '}
— {row.period_start || '?'} /{' '}
{row.period_end || '?'} — v
{row.zone_version}
</option>
))}
</select>
</label>
))}
</div>
<MapObservationComparison first={first} second={second} />
</details>
);
}
export function filterMapIndicators(
records: QuantitativeMapContext['records'],
indicator: string,
@@ -242,6 +474,9 @@ export default function InvestmentMapIndicators({
observações. Refine a localização.
</p>
)}
{rows.length > 1 && (
<MapObservationComparisonPicker rows={rows} />
)}
{rows.length === 0 ? (
<p>
{filtered
@@ -29,6 +29,8 @@ compileFunction(compiled, ['require', 'module', 'exports'])(
);
const Component = module.exports.default;
const filterIndicators = module.exports.filterMapIndicators;
const compareObservations = module.exports.compareMapObservations;
const Comparison = module.exports.MapObservationComparison;
const row = {
id: '1',
zone_id: 'park-a',
@@ -219,3 +221,84 @@ test('quantitative filters are labelled, scoped and explain source date and cove
assert.match(html, /não alteram oportunidades ou limites de cobertura/);
assert.match(html, /Limpar filtros dos indicadores/);
});
test('comparison delta: metadata alignment never manufactures a trend or changes exact source values', () => {
const first = {
...row,
description: 'Emprego observado na fonte sintética.',
value: '1234567890123.456789',
};
const second = {
...first,
id: 'next-period',
period_start: '2026-01-01',
period_end: '2026-12-31',
value: '0',
};
const before = JSON.stringify([first, second]);
assert.equal(compareObservations(first, second).state, 'metadata_aligned');
const html = renderToStaticMarkup(
React.createElement(Comparison, { first, second }),
);
assert.match(html, /1234567890123\.456789/);
assert.match(html, />0</);
assert.match(html, /confirme nas fontes/);
assert.match(html, /não calcula evolução/);
assert.ok(!html.includes('<script>synthetic source</script>'));
assert.match(html, /&lt;script&gt;synthetic source/);
assert.equal(JSON.stringify([first, second]), before);
});
test('comparison delta: differences, missing evidence, unknown values and disputed information require review', () => {
const documented = {
...row,
description: 'Emprego observado na fonte sintética.',
};
const second = { ...documented, id: 'different' };
assert.equal(
compareObservations(documented, second).state,
'metadata_aligned',
);
for (const change of [
{ zone_id: 'other' },
{ indicator: 'exports' },
{ unit: 'USD' },
{ scope: 'other base' },
{ population: 'other population' },
{ methodology: 'other method' },
{ knowledge: 'disputed' },
{ knowledge: 'unknown', value: null },
{ value: 'NaN' },
{ source_reference: '' },
{ description: '' },
{ source_date: '2026-02-30' },
{ period_end: '2024-01-01' },
]) {
const result = compareObservations(documented, {
...second,
...change,
});
assert.equal(result.state, 'review_required', JSON.stringify(change));
assert.ok(result.issues.length);
}
assert.equal(compareObservations(row, row).state, 'review_required');
const html = renderToStaticMarkup(
React.createElement(Comparison, {
first: row,
second: { ...second, knowledge: 'unknown', value: null },
}),
);
assert.match(html, /Por confirmar/);
assert.match(html, /lacunas a conferir/);
});
test('comparison delta: progressive selection stays inside the received filters and shows an incomplete pair explicitly', () => {
const html = render(context);
assert.match(html, /Comparar duas observações/);
assert.match(html, /Primeira.*observação/);
assert.match(html, /Segunda.*observação/);
assert.match(html, /uma versão nova exige nova selecção/);
assert.match(html, /Seleccione duas observações/);
const single = render({ ...context, records: [row] });
assert.ok(!single.includes('Comparar duas observações'));
});