$facts * @param array> $evidence * @return list> */ public function evaluate(array $facts, array $evidence = []): array { $phases = [ 'idea' => ['Definir a actividade e o objectivo', ['project_title', 'project_summary', 'activity'], 'Confirme o que pretende produzir ou prestar.', 'plan'], 'market' => ['Documentar clientes e hipóteses de mercado', ['target_customers', 'market_evidence'], 'Reúna evidência de clientes; uma hipótese não demonstra procura.', 'opportunities'], 'location' => ['Comparar locais e requisitos da actividade', ['location', 'location_evidence'], 'Confirme terreno, acesso, infraestrutura e licenciamento para cada alternativa.', 'locations'], 'resources' => ['Preparar recursos e equipa', ['resource_plan', 'resource_evidence'], 'Distinga recursos declarados de disponibilidade comprovada.', 'partners'], 'finance' => ['Explorar financiamento e cenários', ['funding_evidence'], 'Reveja os pressupostos e os cenários no simulador determinístico.', 'simulator'], 'execution' => ['Reconciliar calendário e dependências', ['expected_start_date', 'ready_date', 'schedule'], 'Confirme datas, dependências e responsáveis antes de assumir marcos.', 'plan'], 'support' => ['Preparar o pedido e acompanhar a resposta', [], 'Prepare um rascunho no Balcão; reveja e submeta pessoalmente.', 'support'], ]; $result = []; foreach ($phases as $code => [$objective, $fields, $explanation, $action]) { $known = []; $unknown = []; $phaseEvidence = []; $risks = []; foreach ($fields as $field) { $value = trim((string) ($facts[$field] ?? '')); $record = $evidence[$field] ?? null; $state = $record['state'] ?? ($value !== '' ? 'investor_declared_not_verified' : 'absent'); if (in_array($field, InvestorGuidanceEvidence::FIELDS, true)) { $phaseEvidence[] = ['field' => $field, 'state' => $state, 'documents' => $record['documents'] ?? []]; if ($state !== 'internally_verified') { $message = match ($state) { 'absent' => 'Ainda não existe informação ou documento para este pressuposto.', 'investor_declared_not_verified' => 'A declaração do investidor ainda não foi conferida.', 'attached_unchecked' => 'Existe um documento associado, sem conferência interna deste pressuposto.', 'expired' => 'A validade documental registada terminou; actualize a evidência.', 'superseded' => 'O ficheiro ou a versão mudou depois da conferência; é necessária nova análise.', 'contradiction' => 'Uma conferência interna identificou contradição no âmbito descrito.', 'issue_identified' => 'Uma conferência interna identificou um problema no âmbito descrito.', default => 'A evidência não está disponível para utilização; consulte o motivo documental.', }; $risks[] = ['code' => $field.'_'.$state, 'classification' => $state === 'contradiction' ? 'documented_contradiction' : 'insufficient_information', 'explanation' => $message, 'field' => $field, 'restriction' => false]; } } if ($value !== '') { $known[] = ['field' => $field, 'value' => $value, 'status' => 'investor_declared_not_verified']; } elseif (! $record) { $unknown[] = $field; } } $result[] = ['id' => $code, 'objective' => $objective, 'known' => $known, 'questions' => $unknown, 'recommendations' => [[ 'code' => $code.'_preparation', 'kind' => 'automatic_recommendation', 'classification' => $unknown ? 'insufficient_information' : 'verification_required', 'explanation' => $explanation, 'action' => $action, 'assumptions' => $known, 'source' => ['identifier' => self::VERSION, 'reference' => 'app/Services/Ai/InvestorGuidanceRules.php#'.$code, 'date' => '2026-10-08', 'nature' => 'internal_preparation_rule'], 'limitations' => ['Uma conferência interna só sustenta o âmbito descrito pelo verificador; não aprova procura, infraestrutura, financiamento, registo ou licença institucional.', 'Sem previsão de sucesso, taxa fiscal ou elegibilidade inferida.'], ]], 'evidence' => $phaseEvidence, 'risks' => $risks]; } if (($facts['expected_start_date'] ?? '') && ($facts['ready_date'] ?? '') && $facts['expected_start_date'] < $facts['ready_date']) { $result[5]['risks'][] = ['code' => 'schedule_contradiction', 'classification' => 'declared_contradiction', 'explanation' => 'O início indicado antecede a disponibilidade declarada dos recursos.', 'basis' => ['expected_start_date' => $facts['expected_start_date'], 'ready_date' => $facts['ready_date']], 'restriction' => false]; } $result[4]['risks'][] = ['code' => 'tax_eligibility_not_inferred', 'classification' => 'insufficient_information', 'explanation' => 'A evidência do financiamento não determina taxas fiscais ou elegibilidade. Consulte os pressupostos, fontes e versões no simulador.', 'restriction' => false]; foreach ([ [1, 'demand_quantity', 'capacity_quantity', ['quantity_unit', 'quantity_period'], 'A procura declarada excede a capacidade declarada para a mesma unidade e período.'], [3, 'resources_required', 'resources_available', ['resource_unit'], 'Os recursos disponíveis declarados são inferiores aos necessários declarados.'], [4, 'funding_required', 'funding_committed', ['funding_currency'], 'O financiamento comprometido declarado é inferior ao necessário declarado na mesma moeda.'], ] as [$phase, $required, $available, $scope, $explanation]) { $values = array_intersect_key($facts, array_flip([$required, $available, ...$scope])); if (! array_filter($values, fn ($value) => $value !== null && $value !== '')) { continue; } $complete = true; foreach ([$required, $available, ...$scope] as $field) { if (! isset($facts[$field]) || trim((string) $facts[$field]) === '') { $complete = false; } } // Compare declared magnitudes only: this is no forecast, valuation or financial calculation. $left = $facts[$required] ?? null; $right = $facts[$available] ?? null; $valid = is_scalar($left) && is_scalar($right) && preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string) $left) && preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string) $right); if (! $complete || ! $valid) { $result[$phase]['risks'][] = ['code' => $required.'_comparison_incomplete', 'classification' => 'insufficient_information', 'explanation' => 'Faltam valores comparáveis, unidade, período ou moeda para esta comparação.', 'basis' => $values, 'restriction' => false]; } elseif ($this->declaredMagnitude((string) $left) > $this->declaredMagnitude((string) $right)) { $result[$phase]['risks'][] = ['code' => $required.'_declared_gap', 'classification' => 'declared_contradiction', 'explanation' => $explanation.' Estes valores são declarações, não disponibilidade comprovada.', 'basis' => $values, 'restriction' => false]; } } foreach ([['completion_date', 'expected_start_date', 'A conclusão declarada antecede o início.'], ['expected_start_date', 'funding_ready_date', 'O início declarado antecede a disponibilidade declarada do financiamento.']] as [$first, $second, $explanation]) { if (! empty($facts[$first]) && ! empty($facts[$second]) && $facts[$first] < $facts[$second]) { $result[5]['risks'][] = ['code' => $first.'_'.$second.'_contradiction', 'classification' => 'declared_contradiction', 'explanation' => $explanation, 'basis' => [$first => $facts[$first], $second => $facts[$second]], 'restriction' => false]; } } return $result; } private function declaredMagnitude(string $value): int { $parts = explode('.', $value); return (int) $parts[0] * 100 + (int) str_pad($parts[1] ?? '', 2, '0'); } }