246 lines
12 KiB
PHP
246 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Ui\OperatorExplanation;
|
|
|
|
use App\Support\Badges\BadgeCatalog;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
|
|
|
|
final class OperatorExplanationBuilder
|
|
{
|
|
/**
|
|
* @param array<int, CountDescriptor> $countDescriptors
|
|
*/
|
|
public function build(
|
|
ExplanationFamily $family,
|
|
string $headline,
|
|
string $executionOutcome,
|
|
string $executionOutcomeLabel,
|
|
string $evaluationResult,
|
|
TrustworthinessLevel $trustworthinessLevel,
|
|
string $reliabilityStatement,
|
|
?string $coverageStatement,
|
|
?string $dominantCauseCode,
|
|
?string $dominantCauseLabel,
|
|
?string $dominantCauseExplanation,
|
|
string $nextActionCategory,
|
|
string $nextActionText,
|
|
array $countDescriptors = [],
|
|
bool $diagnosticsAvailable = false,
|
|
?string $diagnosticsSummary = null,
|
|
): OperatorExplanationPattern {
|
|
return new OperatorExplanationPattern(
|
|
family: $family,
|
|
headline: $headline,
|
|
executionOutcome: $executionOutcome,
|
|
executionOutcomeLabel: $executionOutcomeLabel,
|
|
evaluationResult: $evaluationResult,
|
|
trustworthinessLevel: $trustworthinessLevel,
|
|
reliabilityStatement: $reliabilityStatement,
|
|
coverageStatement: $coverageStatement,
|
|
dominantCauseCode: $dominantCauseCode,
|
|
dominantCauseLabel: $dominantCauseLabel,
|
|
dominantCauseExplanation: $dominantCauseExplanation,
|
|
nextActionCategory: $nextActionCategory,
|
|
nextActionText: $nextActionText,
|
|
countDescriptors: $countDescriptors,
|
|
diagnosticsAvailable: $diagnosticsAvailable,
|
|
diagnosticsSummary: $diagnosticsSummary,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, CountDescriptor> $countDescriptors
|
|
*/
|
|
public function fromArtifactTruthEnvelope(
|
|
ArtifactTruthEnvelope $truth,
|
|
array $countDescriptors = [],
|
|
): OperatorExplanationPattern {
|
|
$reason = $truth->reason?->toReasonResolutionEnvelope();
|
|
$family = $this->familyForTruth($truth, $reason);
|
|
$trustworthiness = $this->trustworthinessForTruth($truth, $reason);
|
|
$evaluationResult = $this->evaluationResultForTruth($truth, $family);
|
|
$executionOutcome = $this->executionOutcomeKey($truth->executionOutcome);
|
|
$executionOutcomeLabel = $this->executionOutcomeLabel($truth->executionOutcome);
|
|
$dominantCauseCode = $reason?->internalCode;
|
|
$dominantCauseLabel = $reason?->operatorLabel ?? $truth->primaryLabel;
|
|
$dominantCauseExplanation = $reason?->shortExplanation ?? $truth->primaryExplanation;
|
|
$headline = $this->headlineForTruth($truth, $family, $trustworthiness);
|
|
$reliabilityStatement = $this->reliabilityStatementForTruth($truth, $trustworthiness);
|
|
$coverageStatement = $this->coverageStatementForTruth($truth, $reason);
|
|
$nextActionText = $truth->nextStepText();
|
|
$nextActionCategory = $this->nextActionCategory($truth->actionability, $reason);
|
|
$diagnosticsAvailable = $truth->reason !== null
|
|
|| $truth->diagnosticLabel !== null
|
|
|| $countDescriptors !== [];
|
|
|
|
return $this->build(
|
|
family: $family,
|
|
headline: $headline,
|
|
executionOutcome: $executionOutcome,
|
|
executionOutcomeLabel: $executionOutcomeLabel,
|
|
evaluationResult: $evaluationResult,
|
|
trustworthinessLevel: $trustworthiness,
|
|
reliabilityStatement: $reliabilityStatement,
|
|
coverageStatement: $coverageStatement,
|
|
dominantCauseCode: $dominantCauseCode,
|
|
dominantCauseLabel: $dominantCauseLabel,
|
|
dominantCauseExplanation: $dominantCauseExplanation,
|
|
nextActionCategory: $nextActionCategory,
|
|
nextActionText: $nextActionText,
|
|
countDescriptors: $countDescriptors,
|
|
diagnosticsAvailable: $diagnosticsAvailable,
|
|
diagnosticsSummary: $diagnosticsAvailable
|
|
? 'Technical truth detail remains available below the primary explanation.'
|
|
: null,
|
|
);
|
|
}
|
|
|
|
private function familyForTruth(
|
|
ArtifactTruthEnvelope $truth,
|
|
?ReasonResolutionEnvelope $reason,
|
|
): ExplanationFamily {
|
|
return match (true) {
|
|
$reason?->absencePattern === 'suppressed_output' => ExplanationFamily::SuppressedOutput,
|
|
$reason?->absencePattern === 'blocked_prerequisite' => ExplanationFamily::BlockedPrerequisite,
|
|
$truth->executionOutcome === 'pending' || $truth->artifactExistence === 'not_created' && $truth->actionability !== 'required' => ExplanationFamily::InProgress,
|
|
$truth->executionOutcome === 'failed' || $truth->executionOutcome === 'blocked' => ExplanationFamily::BlockedPrerequisite,
|
|
$truth->artifactExistence === 'created_but_not_usable' || $truth->contentState === 'missing_input' => ExplanationFamily::MissingInput,
|
|
$truth->contentState === 'trusted' && $truth->freshnessState === 'current' && $truth->actionability === 'none' && $truth->primaryLabel === 'Trustworthy artifact' => ExplanationFamily::TrustworthyResult,
|
|
$truth->contentState === 'trusted' && $truth->freshnessState === 'current' && $truth->actionability === 'none' => ExplanationFamily::NoIssuesDetected,
|
|
$truth->artifactExistence === 'historical_only' => ExplanationFamily::Unavailable,
|
|
default => ExplanationFamily::CompletedButLimited,
|
|
};
|
|
}
|
|
|
|
private function trustworthinessForTruth(
|
|
ArtifactTruthEnvelope $truth,
|
|
?ReasonResolutionEnvelope $reason,
|
|
): TrustworthinessLevel {
|
|
if ($reason?->trustImpact !== null) {
|
|
return TrustworthinessLevel::tryFrom($reason->trustImpact) ?? TrustworthinessLevel::LimitedConfidence;
|
|
}
|
|
|
|
return match (true) {
|
|
$truth->artifactExistence === 'created_but_not_usable',
|
|
$truth->contentState === 'missing_input',
|
|
$truth->executionOutcome === 'failed',
|
|
$truth->executionOutcome === 'blocked' => TrustworthinessLevel::Unusable,
|
|
$truth->supportState === 'limited_support',
|
|
in_array($truth->contentState, ['reference_only', 'unsupported'], true) => TrustworthinessLevel::DiagnosticOnly,
|
|
$truth->contentState === 'trusted' && $truth->freshnessState === 'current' && $truth->actionability === 'none' => TrustworthinessLevel::Trustworthy,
|
|
default => TrustworthinessLevel::LimitedConfidence,
|
|
};
|
|
}
|
|
|
|
private function evaluationResultForTruth(
|
|
ArtifactTruthEnvelope $truth,
|
|
ExplanationFamily $family,
|
|
): string {
|
|
return match ($family) {
|
|
ExplanationFamily::TrustworthyResult => 'full_result',
|
|
ExplanationFamily::NoIssuesDetected => 'no_result',
|
|
ExplanationFamily::SuppressedOutput => 'suppressed_result',
|
|
ExplanationFamily::MissingInput,
|
|
ExplanationFamily::BlockedPrerequisite,
|
|
ExplanationFamily::Unavailable => 'unavailable',
|
|
ExplanationFamily::InProgress => 'unavailable',
|
|
ExplanationFamily::CompletedButLimited => 'incomplete_result',
|
|
};
|
|
}
|
|
|
|
private function executionOutcomeKey(?string $executionOutcome): string
|
|
{
|
|
$normalized = BadgeCatalog::normalizeState($executionOutcome);
|
|
|
|
return match ($normalized) {
|
|
'queued', 'running', 'pending' => 'in_progress',
|
|
'partially_succeeded' => 'completed_with_follow_up',
|
|
'blocked' => 'blocked',
|
|
'failed' => 'failed',
|
|
default => 'completed',
|
|
};
|
|
}
|
|
|
|
private function executionOutcomeLabel(?string $executionOutcome): string
|
|
{
|
|
if (! is_string($executionOutcome) || trim($executionOutcome) === '') {
|
|
return 'Completed';
|
|
}
|
|
|
|
$spec = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $executionOutcome);
|
|
|
|
return $spec->label !== 'Unknown' ? $spec->label : ucfirst(str_replace('_', ' ', trim($executionOutcome)));
|
|
}
|
|
|
|
private function headlineForTruth(
|
|
ArtifactTruthEnvelope $truth,
|
|
ExplanationFamily $family,
|
|
TrustworthinessLevel $trustworthiness,
|
|
): string {
|
|
return match ($family) {
|
|
ExplanationFamily::TrustworthyResult => 'The result is ready to use.',
|
|
ExplanationFamily::NoIssuesDetected => 'No follow-up was detected from this result.',
|
|
ExplanationFamily::SuppressedOutput => 'The run completed, but normal output was intentionally suppressed.',
|
|
ExplanationFamily::MissingInput => 'The result exists, but missing inputs keep it from being decision-grade.',
|
|
ExplanationFamily::BlockedPrerequisite => 'The workflow did not produce a usable result because a prerequisite blocked it.',
|
|
ExplanationFamily::InProgress => 'The result is still being prepared.',
|
|
ExplanationFamily::Unavailable => 'A result is not currently available for this surface.',
|
|
ExplanationFamily::CompletedButLimited => match ($trustworthiness) {
|
|
TrustworthinessLevel::DiagnosticOnly => 'The result is available for diagnostics, not for a final decision.',
|
|
TrustworthinessLevel::LimitedConfidence => 'The result is available, but it should be read with caution.',
|
|
TrustworthinessLevel::Unusable => 'The result is not reliable enough to use as-is.',
|
|
default => 'The result completed with operator follow-up.',
|
|
},
|
|
};
|
|
}
|
|
|
|
private function reliabilityStatementForTruth(
|
|
ArtifactTruthEnvelope $truth,
|
|
TrustworthinessLevel $trustworthiness,
|
|
): string {
|
|
return match ($trustworthiness) {
|
|
TrustworthinessLevel::Trustworthy => 'Trustworthiness is high for the intended operator task.',
|
|
TrustworthinessLevel::LimitedConfidence => $truth->primaryExplanation
|
|
?? 'Trustworthiness is limited because coverage, freshness, or publication readiness still need review.',
|
|
TrustworthinessLevel::DiagnosticOnly => 'This output is suitable for diagnostics only and should not be treated as the final answer.',
|
|
TrustworthinessLevel::Unusable => 'This output is not reliable enough to support the intended operator action yet.',
|
|
};
|
|
}
|
|
|
|
private function coverageStatementForTruth(
|
|
ArtifactTruthEnvelope $truth,
|
|
?ReasonResolutionEnvelope $reason,
|
|
): ?string {
|
|
return match (true) {
|
|
$truth->contentState === 'trusted' && $truth->freshnessState === 'current' => 'Coverage and artifact quality are sufficient for the default reading path.',
|
|
$truth->freshnessState === 'stale' => 'The artifact exists, but freshness limits how confidently it should be used.',
|
|
$truth->contentState === 'partial' => 'Coverage is incomplete, so the visible output should be treated as partial.',
|
|
$truth->contentState === 'missing_input' => $reason?->shortExplanation ?? 'Required inputs were missing or unusable when this result was assembled.',
|
|
in_array($truth->contentState, ['reference_only', 'unsupported'], true) => 'Only reduced-fidelity support is available for this result.',
|
|
$truth->publicationReadiness === 'blocked' => 'The artifact exists, but it is still blocked from the intended downstream use.',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function nextActionCategory(
|
|
string $actionability,
|
|
?ReasonResolutionEnvelope $reason,
|
|
): string {
|
|
if ($reason?->actionability === 'retryable_transient') {
|
|
return 'retry_later';
|
|
}
|
|
|
|
return match ($actionability) {
|
|
'none' => 'none',
|
|
'optional' => 'review_evidence_gaps',
|
|
default => $reason?->actionability === 'prerequisite_missing'
|
|
? 'fix_prerequisite'
|
|
: 'manual_validate',
|
|
};
|
|
}
|
|
}
|