feat: add governance run summaries
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 43s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 43s
This commit is contained in:
parent
c86b399b43
commit
c6cc58e1f3
4
.github/agents/copilot-instructions.md
vendored
4
.github/agents/copilot-instructions.md
vendored
@ -222,6 +222,8 @@ ## Active Technologies
|
|||||||
- Static filesystem content, Astro content collections, and assets under `apps/website/src` and `apps/website/public`; no database (216-homepage-structure)
|
- Static filesystem content, Astro content collections, and assets under `apps/website/src` and `apps/website/public`; no database (216-homepage-structure)
|
||||||
- PHP 8.4.15 / Laravel 12 + Filament v5, Livewire v4.0+, Pest v4, Tailwind CSS v4 (219-finding-ownership-semantics)
|
- PHP 8.4.15 / Laravel 12 + Filament v5, Livewire v4.0+, Pest v4, Tailwind CSS v4 (219-finding-ownership-semantics)
|
||||||
- PostgreSQL via Sail; existing `findings.owner_user_id`, `findings.assignee_user_id`, and `finding_exceptions.owner_user_id` fields; no schema changes planned (219-finding-ownership-semantics)
|
- PostgreSQL via Sail; existing `findings.owner_user_id`, `findings.assignee_user_id`, and `finding_exceptions.owner_user_id` fields; no schema changes planned (219-finding-ownership-semantics)
|
||||||
|
- PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + Filament v5, Livewire v4, Pest v4, Laravel Sail, `TenantlessOperationRunViewer`, `OperationRunResource`, `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, `ReasonPresenter`, `OperationUxPresenter`, `SummaryCountsNormalizer`, and the existing enterprise-detail builders (220-governance-run-summaries)
|
||||||
|
- PostgreSQL via existing `operation_runs` plus related `baseline_snapshots`, `evidence_snapshots`, `tenant_reviews`, and `review_packs`; no schema changes planned (220-governance-run-summaries)
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -256,9 +258,9 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 220-governance-run-summaries: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade + Filament v5, Livewire v4, Pest v4, Laravel Sail, `TenantlessOperationRunViewer`, `OperationRunResource`, `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, `ReasonPresenter`, `OperationUxPresenter`, `SummaryCountsNormalizer`, and the existing enterprise-detail builders
|
||||||
- 219-finding-ownership-semantics: Added PHP 8.4.15 / Laravel 12 + Filament v5, Livewire v4.0+, Pest v4, Tailwind CSS v4
|
- 219-finding-ownership-semantics: Added PHP 8.4.15 / Laravel 12 + Filament v5, Livewire v4.0+, Pest v4, Tailwind CSS v4
|
||||||
- 216-provider-dispatch-gate: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + Filament Resources/Pages/Actions, Livewire 4, Pest 4, `ProviderOperationStartGate`, `ProviderOperationRegistry`, `ProviderConnectionResolver`, `OperationRunService`, `ProviderNextStepsRegistry`, `ReasonPresenter`, `OperationUxPresenter`, `OperationRunLinks`
|
- 216-provider-dispatch-gate: Added PHP 8.4.15, Laravel 12, Filament v5, Livewire v4 + Filament Resources/Pages/Actions, Livewire 4, Pest 4, `ProviderOperationStartGate`, `ProviderOperationRegistry`, `ProviderConnectionResolver`, `OperationRunService`, `ProviderNextStepsRegistry`, `ReasonPresenter`, `OperationUxPresenter`, `OperationRunLinks`
|
||||||
- 216-homepage-structure: Added Astro 6.0.0 templates + TypeScript 5.9.x + Astro 6, Tailwind CSS v4, local Astro layout/section primitives, Astro content collections, Playwright browser smoke tests
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|
||||||
### Pre-production compatibility check
|
### Pre-production compatibility check
|
||||||
|
|||||||
@ -246,21 +246,10 @@ public function blockedExecutionBanner(): ?array
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$operatorExplanation = $this->governanceOperatorExplanation();
|
|
||||||
$reasonEnvelope = app(ReasonPresenter::class)->forOperationRun($this->run, 'run_detail');
|
|
||||||
$lines = $operatorExplanation instanceof OperatorExplanationPattern
|
|
||||||
? array_values(array_filter([
|
|
||||||
$operatorExplanation->headline,
|
|
||||||
$operatorExplanation->dominantCauseExplanation,
|
|
||||||
]))
|
|
||||||
: ($reasonEnvelope?->toBodyLines(false) ?? [
|
|
||||||
$this->surfaceFailureDetail() ?? 'The queued operation was refused before side effects could begin.',
|
|
||||||
]);
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'tone' => 'amber',
|
'tone' => 'amber',
|
||||||
'title' => 'Blocked by prerequisite',
|
'title' => 'Blocked by prerequisite',
|
||||||
'body' => implode(' ', array_values(array_unique($lines))),
|
'body' => 'This run was blocked before the artifact-producing work could finish. Review the summary below for the dominant blocker and next step.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -280,16 +280,25 @@ private static function enterpriseDetailPage(OperationRun $record): \App\Support
|
|||||||
: null;
|
: null;
|
||||||
$artifactTruth = static::artifactTruthEnvelope($record);
|
$artifactTruth = static::artifactTruthEnvelope($record);
|
||||||
$operatorExplanation = $artifactTruth?->operatorExplanation;
|
$operatorExplanation = $artifactTruth?->operatorExplanation;
|
||||||
|
$diagnosticSummary = OperationUxPresenter::governanceDiagnosticSummary($record);
|
||||||
$reasonEnvelope = app(ReasonPresenter::class)->forOperationRun($record, 'run_detail');
|
$reasonEnvelope = app(ReasonPresenter::class)->forOperationRun($record, 'run_detail');
|
||||||
$primaryNextStep = static::resolvePrimaryNextStep($record, $artifactTruth, $operatorExplanation);
|
$primaryNextStep = static::resolvePrimaryNextStep($record, $artifactTruth, $operatorExplanation);
|
||||||
|
$decisionNextStep = $diagnosticSummary instanceof \App\Support\OpsUx\GovernanceRunDiagnosticSummary
|
||||||
|
? [
|
||||||
|
'text' => $diagnosticSummary->nextActionText,
|
||||||
|
'source' => $diagnosticSummary->nextActionCategory,
|
||||||
|
'secondaryGuidance' => $primaryNextStep['secondaryGuidance'],
|
||||||
|
]
|
||||||
|
: $primaryNextStep;
|
||||||
$restoreContinuation = static::restoreContinuation($record);
|
$restoreContinuation = static::restoreContinuation($record);
|
||||||
$supportingGroups = static::supportingGroups(
|
$supportingGroups = static::supportingGroups(
|
||||||
record: $record,
|
record: $record,
|
||||||
factory: $factory,
|
factory: $factory,
|
||||||
referencedTenantLifecycle: $referencedTenantLifecycle,
|
referencedTenantLifecycle: $referencedTenantLifecycle,
|
||||||
|
diagnosticSummary: $diagnosticSummary,
|
||||||
operatorExplanation: $operatorExplanation,
|
operatorExplanation: $operatorExplanation,
|
||||||
reasonEnvelope: $reasonEnvelope,
|
reasonEnvelope: $reasonEnvelope,
|
||||||
primaryNextStep: $primaryNextStep,
|
primaryNextStep: $decisionNextStep,
|
||||||
);
|
);
|
||||||
|
|
||||||
$builder = \App\Support\Ui\EnterpriseDetail\EnterpriseDetailBuilder::make('operation_run', 'workspace-context')
|
$builder = \App\Support\Ui\EnterpriseDetail\EnterpriseDetailBuilder::make('operation_run', 'workspace-context')
|
||||||
@ -307,49 +316,25 @@ private static function enterpriseDetailPage(OperationRun $record): \App\Support
|
|||||||
descriptionHint: 'Decision guidance and high-signal context stay ahead of diagnostic payloads and raw JSON.',
|
descriptionHint: 'Decision guidance and high-signal context stay ahead of diagnostic payloads and raw JSON.',
|
||||||
))
|
))
|
||||||
->decisionZone($factory->decisionZone(
|
->decisionZone($factory->decisionZone(
|
||||||
facts: array_values(array_filter([
|
facts: static::decisionFacts(
|
||||||
$factory->keyFact(
|
factory: $factory,
|
||||||
'Execution state',
|
record: $record,
|
||||||
$statusSpec->label,
|
statusSpec: $statusSpec,
|
||||||
badge: $factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
|
outcomeSpec: $outcomeSpec,
|
||||||
),
|
artifactTruth: $artifactTruth,
|
||||||
$factory->keyFact(
|
operatorExplanation: $operatorExplanation,
|
||||||
'Outcome',
|
restoreContinuation: $restoreContinuation,
|
||||||
$outcomeSpec->label,
|
diagnosticSummary: $diagnosticSummary,
|
||||||
badge: $factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor),
|
|
||||||
),
|
|
||||||
static::artifactTruthFact($factory, $artifactTruth),
|
|
||||||
$operatorExplanation instanceof OperatorExplanationPattern
|
|
||||||
? $factory->keyFact(
|
|
||||||
'Result meaning',
|
|
||||||
$operatorExplanation->evaluationResultLabel(),
|
|
||||||
$operatorExplanation->headline,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
$operatorExplanation instanceof OperatorExplanationPattern
|
|
||||||
? $factory->keyFact(
|
|
||||||
'Result trust',
|
|
||||||
$operatorExplanation->trustworthinessLabel(),
|
|
||||||
static::detailHintUnlessDuplicate(
|
|
||||||
$operatorExplanation->reliabilityStatement,
|
|
||||||
$artifactTruth?->primaryExplanation,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
is_array($restoreContinuation)
|
|
||||||
? $factory->keyFact(
|
|
||||||
'Restore continuation',
|
|
||||||
(string) ($restoreContinuation['badge_label'] ?? 'Restore detail'),
|
|
||||||
(string) ($restoreContinuation['summary'] ?? 'Restore continuation detail is unavailable.'),
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
])),
|
|
||||||
primaryNextStep: $factory->primaryNextStep(
|
|
||||||
$primaryNextStep['text'],
|
|
||||||
$primaryNextStep['source'],
|
|
||||||
$primaryNextStep['secondaryGuidance'],
|
|
||||||
),
|
),
|
||||||
description: 'Start here to see how the operation ended, whether the result is trustworthy enough to use, and the one primary next step.',
|
primaryNextStep: $factory->primaryNextStep(
|
||||||
|
$decisionNextStep['text'],
|
||||||
|
$decisionNextStep['source'],
|
||||||
|
$decisionNextStep['secondaryGuidance'],
|
||||||
|
'Primary next step',
|
||||||
|
),
|
||||||
|
description: $diagnosticSummary instanceof \App\Support\OpsUx\GovernanceRunDiagnosticSummary
|
||||||
|
? 'Start here to see what happened, how reliable the resulting artifact is, what was affected, and the one next step.'
|
||||||
|
: 'Start here to see how the operation ended, whether the result is trustworthy enough to use, and the one primary next step.',
|
||||||
compactCounts: $summaryLine !== null
|
compactCounts: $summaryLine !== null
|
||||||
? $factory->countPresentation(summaryLine: $summaryLine)
|
? $factory->countPresentation(summaryLine: $summaryLine)
|
||||||
: null,
|
: null,
|
||||||
@ -550,6 +535,7 @@ private static function supportingGroups(
|
|||||||
OperationRun $record,
|
OperationRun $record,
|
||||||
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
|
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
|
||||||
?ReferencedTenantLifecyclePresentation $referencedTenantLifecycle,
|
?ReferencedTenantLifecyclePresentation $referencedTenantLifecycle,
|
||||||
|
?\App\Support\OpsUx\GovernanceRunDiagnosticSummary $diagnosticSummary,
|
||||||
?OperatorExplanationPattern $operatorExplanation,
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
?ReasonResolutionEnvelope $reasonEnvelope,
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
array $primaryNextStep,
|
array $primaryNextStep,
|
||||||
@ -559,6 +545,21 @@ private static function supportingGroups(
|
|||||||
$reasonSemantics = app(ReasonPresenter::class)->semantics($reasonEnvelope);
|
$reasonSemantics = app(ReasonPresenter::class)->semantics($reasonEnvelope);
|
||||||
|
|
||||||
$guidanceItems = array_values(array_filter([
|
$guidanceItems = array_values(array_filter([
|
||||||
|
...array_map(
|
||||||
|
static fn (array $fact): array => $factory->keyFact(
|
||||||
|
(string) ($fact['label'] ?? 'Summary detail'),
|
||||||
|
(string) ($fact['value'] ?? '—'),
|
||||||
|
is_string($fact['hint'] ?? null) ? $fact['hint'] : null,
|
||||||
|
tone: match ($fact['emphasis'] ?? null) {
|
||||||
|
'blocked' => 'danger',
|
||||||
|
'caution' => 'warning',
|
||||||
|
default => null,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
$diagnosticSummary instanceof \App\Support\OpsUx\GovernanceRunDiagnosticSummary
|
||||||
|
? array_values(array_filter($diagnosticSummary->secondaryFacts, 'is_array'))
|
||||||
|
: [],
|
||||||
|
),
|
||||||
$operatorExplanation instanceof OperatorExplanationPattern && $operatorExplanation->coverageStatement !== null
|
$operatorExplanation instanceof OperatorExplanationPattern && $operatorExplanation->coverageStatement !== null
|
||||||
? $factory->keyFact('Coverage', $operatorExplanation->coverageStatement)
|
? $factory->keyFact('Coverage', $operatorExplanation->coverageStatement)
|
||||||
: null,
|
: null,
|
||||||
@ -811,6 +812,8 @@ private static function guidanceLabel(string $source): string
|
|||||||
private static function artifactTruthFact(
|
private static function artifactTruthFact(
|
||||||
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
|
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
|
||||||
?ArtifactTruthEnvelope $artifactTruth,
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?string $hintOverride = null,
|
||||||
|
bool $preferOverride = false,
|
||||||
): ?array {
|
): ?array {
|
||||||
if (! $artifactTruth instanceof ArtifactTruthEnvelope) {
|
if (! $artifactTruth instanceof ArtifactTruthEnvelope) {
|
||||||
return null;
|
return null;
|
||||||
@ -823,19 +826,138 @@ private static function artifactTruthFact(
|
|||||||
$badge = $outcome->primaryBadge;
|
$badge = $outcome->primaryBadge;
|
||||||
|
|
||||||
return $factory->keyFact(
|
return $factory->keyFact(
|
||||||
'Outcome',
|
'Artifact impact',
|
||||||
$outcome->primaryLabel,
|
$outcome->primaryLabel,
|
||||||
$outcome->primaryReason,
|
$preferOverride ? $hintOverride : ($hintOverride ?? $outcome->primaryReason),
|
||||||
$factory->statusBadge($badge->label, $badge->color, $badge->icon, $badge->iconColor),
|
$factory->statusBadge($badge->label, $badge->color, $badge->icon, $badge->iconColor),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private static function decisionFacts(
|
||||||
|
\App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory $factory,
|
||||||
|
OperationRun $record,
|
||||||
|
\App\Support\Badges\BadgeSpec $statusSpec,
|
||||||
|
\App\Support\Badges\BadgeSpec $outcomeSpec,
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
mixed $restoreContinuation,
|
||||||
|
?\App\Support\OpsUx\GovernanceRunDiagnosticSummary $diagnosticSummary,
|
||||||
|
): array {
|
||||||
|
if (! $diagnosticSummary instanceof \App\Support\OpsUx\GovernanceRunDiagnosticSummary) {
|
||||||
|
return array_values(array_filter([
|
||||||
|
$factory->keyFact(
|
||||||
|
'Execution state',
|
||||||
|
$statusSpec->label,
|
||||||
|
badge: $factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
|
||||||
|
),
|
||||||
|
$factory->keyFact(
|
||||||
|
'Outcome',
|
||||||
|
$outcomeSpec->label,
|
||||||
|
badge: $factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor),
|
||||||
|
),
|
||||||
|
static::artifactTruthFact($factory, $artifactTruth),
|
||||||
|
$operatorExplanation instanceof OperatorExplanationPattern
|
||||||
|
? $factory->keyFact(
|
||||||
|
'Result meaning',
|
||||||
|
$operatorExplanation->evaluationResultLabel(),
|
||||||
|
$operatorExplanation->headline,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
$operatorExplanation instanceof OperatorExplanationPattern
|
||||||
|
? $factory->keyFact(
|
||||||
|
'Result trust',
|
||||||
|
$operatorExplanation->trustworthinessLabel(),
|
||||||
|
static::detailHintUnlessDuplicate(
|
||||||
|
$operatorExplanation->reliabilityStatement,
|
||||||
|
$artifactTruth?->primaryExplanation,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
is_array($restoreContinuation)
|
||||||
|
? $factory->keyFact(
|
||||||
|
'Restore continuation',
|
||||||
|
(string) ($restoreContinuation['badge_label'] ?? 'Restore detail'),
|
||||||
|
(string) ($restoreContinuation['summary'] ?? 'Restore continuation detail is unavailable.'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$facts = [
|
||||||
|
$factory->keyFact(
|
||||||
|
'Execution state',
|
||||||
|
$statusSpec->label,
|
||||||
|
badge: $factory->statusBadge($statusSpec->label, $statusSpec->color, $statusSpec->icon, $statusSpec->iconColor),
|
||||||
|
),
|
||||||
|
$factory->keyFact(
|
||||||
|
'Outcome',
|
||||||
|
$diagnosticSummary->executionOutcomeLabel,
|
||||||
|
badge: $factory->statusBadge($outcomeSpec->label, $outcomeSpec->color, $outcomeSpec->icon, $outcomeSpec->iconColor),
|
||||||
|
),
|
||||||
|
static::artifactTruthFact(
|
||||||
|
$factory,
|
||||||
|
$artifactTruth,
|
||||||
|
static::detailHintUnlessDuplicate(
|
||||||
|
$diagnosticSummary->headline,
|
||||||
|
$artifactTruth?->primaryExplanation,
|
||||||
|
$diagnosticSummary->primaryReason,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
$factory->keyFact(
|
||||||
|
'Dominant cause',
|
||||||
|
$diagnosticSummary->dominantCause['label'],
|
||||||
|
$diagnosticSummary->primaryReason,
|
||||||
|
tone: in_array($diagnosticSummary->nextActionCategory, ['refresh_prerequisite_data', 'review_scope_or_ambiguous_matches'], true)
|
||||||
|
? 'warning'
|
||||||
|
: (in_array($diagnosticSummary->nextActionCategory, ['retry_later', 'no_further_action'], true) ? null : 'danger'),
|
||||||
|
),
|
||||||
|
$operatorExplanation instanceof OperatorExplanationPattern
|
||||||
|
? $factory->keyFact(
|
||||||
|
'Result trust',
|
||||||
|
$operatorExplanation->trustworthinessLabel(),
|
||||||
|
static::detailHintUnlessDuplicate(
|
||||||
|
$operatorExplanation->reliabilityStatement,
|
||||||
|
$diagnosticSummary->primaryReason,
|
||||||
|
),
|
||||||
|
tone: match ($operatorExplanation->trustworthinessLevel->value) {
|
||||||
|
'unusable' => 'danger',
|
||||||
|
'diagnostic_only', 'limited_confidence' => 'warning',
|
||||||
|
default => 'success',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
is_array($restoreContinuation)
|
||||||
|
? $factory->keyFact(
|
||||||
|
'Restore continuation',
|
||||||
|
(string) ($restoreContinuation['badge_label'] ?? 'Restore detail'),
|
||||||
|
(string) ($restoreContinuation['summary'] ?? 'Restore continuation detail is unavailable.'),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (is_array($diagnosticSummary->affectedScaleCue)) {
|
||||||
|
$source = str_replace('_', ' ', (string) ($diagnosticSummary->affectedScaleCue['source'] ?? 'recorded detail'));
|
||||||
|
|
||||||
|
$facts[] = $factory->keyFact(
|
||||||
|
(string) ($diagnosticSummary->affectedScaleCue['label'] ?? 'Affected scale'),
|
||||||
|
(string) ($diagnosticSummary->affectedScaleCue['value'] ?? 'Recorded detail is available.'),
|
||||||
|
'Backed by '.$source.'.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter($facts));
|
||||||
|
}
|
||||||
|
|
||||||
private static function decisionAttentionNote(OperationRun $record): ?string
|
private static function decisionAttentionNote(OperationRun $record): ?string
|
||||||
{
|
{
|
||||||
return OperationUxPresenter::decisionAttentionNote($record);
|
return OperationUxPresenter::decisionAttentionNote($record);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static function detailHintUnlessDuplicate(?string $hint, ?string $duplicateOf): ?string
|
private static function detailHintUnlessDuplicate(?string $hint, ?string ...$duplicates): ?string
|
||||||
{
|
{
|
||||||
$normalizedHint = static::normalizeDetailText($hint);
|
$normalizedHint = static::normalizeDetailText($hint);
|
||||||
|
|
||||||
@ -843,8 +965,10 @@ private static function detailHintUnlessDuplicate(?string $hint, ?string $duplic
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($normalizedHint === static::normalizeDetailText($duplicateOf)) {
|
foreach ($duplicates as $duplicate) {
|
||||||
return null;
|
if ($normalizedHint === static::normalizeDetailText($duplicate)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return trim($hint ?? '');
|
return trim($hint ?? '');
|
||||||
|
|||||||
@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\OpsUx;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
final readonly class GovernanceRunDiagnosticSummary
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{label: string, value: string, source: string, confidence?: string}|null $affectedScaleCue
|
||||||
|
* @param array{
|
||||||
|
* code: ?string,
|
||||||
|
* label: string,
|
||||||
|
* explanation: string
|
||||||
|
* } $dominantCause
|
||||||
|
* @param list<array{
|
||||||
|
* code: ?string,
|
||||||
|
* label: string,
|
||||||
|
* explanation: string
|
||||||
|
* }> $secondaryCauses
|
||||||
|
* @param list<array{
|
||||||
|
* label: string,
|
||||||
|
* value: string,
|
||||||
|
* hint?: ?string,
|
||||||
|
* emphasis?: string
|
||||||
|
* }> $secondaryFacts
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public string $headline,
|
||||||
|
public string $executionOutcomeLabel,
|
||||||
|
public string $artifactImpactLabel,
|
||||||
|
public string $primaryReason,
|
||||||
|
public ?array $affectedScaleCue,
|
||||||
|
public string $nextActionCategory,
|
||||||
|
public string $nextActionText,
|
||||||
|
public array $dominantCause,
|
||||||
|
public array $secondaryCauses = [],
|
||||||
|
public array $secondaryFacts = [],
|
||||||
|
public bool $diagnosticsAvailable = false,
|
||||||
|
) {
|
||||||
|
foreach ([
|
||||||
|
'headline' => $this->headline,
|
||||||
|
'executionOutcomeLabel' => $this->executionOutcomeLabel,
|
||||||
|
'artifactImpactLabel' => $this->artifactImpactLabel,
|
||||||
|
'primaryReason' => $this->primaryReason,
|
||||||
|
'nextActionCategory' => $this->nextActionCategory,
|
||||||
|
'nextActionText' => $this->nextActionText,
|
||||||
|
] as $field => $value) {
|
||||||
|
if (trim($value) === '') {
|
||||||
|
throw new InvalidArgumentException("Governance run summaries require {$field}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trim((string) ($this->dominantCause['label'] ?? '')) === '' || trim((string) ($this->dominantCause['explanation'] ?? '')) === '') {
|
||||||
|
throw new InvalidArgumentException('Governance run summaries require a dominant cause label and explanation.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* headline: string,
|
||||||
|
* executionOutcomeLabel: string,
|
||||||
|
* artifactImpactLabel: string,
|
||||||
|
* primaryReason: string,
|
||||||
|
* affectedScaleCue: array{label: string, value: string, source: string, confidence?: string}|null,
|
||||||
|
* nextActionCategory: string,
|
||||||
|
* nextActionText: string,
|
||||||
|
* dominantCause: array{code: ?string, label: string, explanation: string},
|
||||||
|
* secondaryCauses: list<array{code: ?string, label: string, explanation: string}>,
|
||||||
|
* secondaryFacts: list<array{label: string, value: string, hint?: ?string, emphasis?: string}>,
|
||||||
|
* diagnosticsAvailable: bool
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'headline' => $this->headline,
|
||||||
|
'executionOutcomeLabel' => $this->executionOutcomeLabel,
|
||||||
|
'artifactImpactLabel' => $this->artifactImpactLabel,
|
||||||
|
'primaryReason' => $this->primaryReason,
|
||||||
|
'affectedScaleCue' => $this->affectedScaleCue,
|
||||||
|
'nextActionCategory' => $this->nextActionCategory,
|
||||||
|
'nextActionText' => $this->nextActionText,
|
||||||
|
'dominantCause' => $this->dominantCause,
|
||||||
|
'secondaryCauses' => $this->secondaryCauses,
|
||||||
|
'secondaryFacts' => $this->secondaryFacts,
|
||||||
|
'diagnosticsAvailable' => $this->diagnosticsAvailable,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,913 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\OpsUx;
|
||||||
|
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeDomain;
|
||||||
|
use App\Support\Baselines\BaselineCompareReasonCode;
|
||||||
|
use App\Support\OperationCatalog;
|
||||||
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
||||||
|
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
|
||||||
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
|
||||||
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
||||||
|
use App\Support\Ui\OperatorExplanation\CountDescriptor;
|
||||||
|
use App\Support\Ui\OperatorExplanation\OperatorExplanationPattern;
|
||||||
|
|
||||||
|
final class GovernanceRunDiagnosticSummaryBuilder
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ArtifactTruthPresenter $artifactTruthPresenter,
|
||||||
|
private readonly ReasonPresenter $reasonPresenter,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function build(
|
||||||
|
OperationRun $run,
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth = null,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation = null,
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope = null,
|
||||||
|
): ?GovernanceRunDiagnosticSummary {
|
||||||
|
if (! $run->supportsOperatorExplanation()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$artifactTruth ??= $this->artifactTruthPresenter->forOperationRun($run);
|
||||||
|
$operatorExplanation ??= $artifactTruth?->operatorExplanation;
|
||||||
|
$reasonEnvelope ??= $this->reasonPresenter->forOperationRun($run, 'run_detail');
|
||||||
|
|
||||||
|
if (! $artifactTruth instanceof ArtifactTruthEnvelope && ! $operatorExplanation instanceof OperatorExplanationPattern) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$canonicalType = OperationCatalog::canonicalCode((string) $run->type);
|
||||||
|
$context = is_array($run->context) ? $run->context : [];
|
||||||
|
$counts = SummaryCountsNormalizer::normalize(is_array($run->summary_counts) ? $run->summary_counts : []);
|
||||||
|
$causeCandidates = $this->rankCauseCandidates($canonicalType, $run, $artifactTruth, $operatorExplanation, $reasonEnvelope, $context);
|
||||||
|
$dominantCause = $causeCandidates[0] ?? $this->fallbackCause($artifactTruth, $operatorExplanation, $reasonEnvelope);
|
||||||
|
$secondaryCauses = array_values(array_slice($causeCandidates, 1));
|
||||||
|
$artifactImpactLabel = $this->artifactImpactLabel($artifactTruth, $operatorExplanation);
|
||||||
|
$headline = $this->headline($canonicalType, $run, $artifactTruth, $operatorExplanation, $dominantCause, $context, $counts);
|
||||||
|
$primaryReason = $this->primaryReason($dominantCause, $artifactTruth, $operatorExplanation, $reasonEnvelope);
|
||||||
|
$nextActionCategory = $this->nextActionCategory($canonicalType, $run, $reasonEnvelope, $operatorExplanation, $context);
|
||||||
|
$nextActionText = $this->nextActionText($artifactTruth, $operatorExplanation, $reasonEnvelope);
|
||||||
|
$affectedScaleCue = $this->affectedScaleCue($canonicalType, $run, $artifactTruth, $operatorExplanation, $context, $counts);
|
||||||
|
$secondaryFacts = $this->secondaryFacts($artifactTruth, $operatorExplanation, $secondaryCauses, $nextActionCategory, $nextActionText);
|
||||||
|
|
||||||
|
return new GovernanceRunDiagnosticSummary(
|
||||||
|
headline: $headline,
|
||||||
|
executionOutcomeLabel: $this->executionOutcomeLabel($run),
|
||||||
|
artifactImpactLabel: $artifactImpactLabel,
|
||||||
|
primaryReason: $primaryReason,
|
||||||
|
affectedScaleCue: $affectedScaleCue,
|
||||||
|
nextActionCategory: $nextActionCategory,
|
||||||
|
nextActionText: $nextActionText,
|
||||||
|
dominantCause: [
|
||||||
|
'code' => $dominantCause['code'] ?? null,
|
||||||
|
'label' => $dominantCause['label'],
|
||||||
|
'explanation' => $dominantCause['explanation'],
|
||||||
|
],
|
||||||
|
secondaryCauses: array_map(
|
||||||
|
static fn (array $cause): array => [
|
||||||
|
'code' => $cause['code'] ?? null,
|
||||||
|
'label' => $cause['label'],
|
||||||
|
'explanation' => $cause['explanation'],
|
||||||
|
],
|
||||||
|
$secondaryCauses,
|
||||||
|
),
|
||||||
|
secondaryFacts: $secondaryFacts,
|
||||||
|
diagnosticsAvailable: (bool) ($operatorExplanation?->diagnosticsAvailable ?? false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function executionOutcomeLabel(OperationRun $run): string
|
||||||
|
{
|
||||||
|
$spec = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, (string) $run->outcome);
|
||||||
|
|
||||||
|
return $spec->label !== 'Unknown'
|
||||||
|
? $spec->label
|
||||||
|
: ucfirst(str_replace('_', ' ', trim((string) $run->outcome)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function artifactImpactLabel(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): string {
|
||||||
|
if ($artifactTruth instanceof ArtifactTruthEnvelope && trim($artifactTruth->primaryLabel) !== '') {
|
||||||
|
return $artifactTruth->primaryLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($operatorExplanation instanceof OperatorExplanationPattern) {
|
||||||
|
return $operatorExplanation->trustworthinessLabel();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Result needs review';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{code: ?string, label: string, explanation: string} $dominantCause
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
*/
|
||||||
|
private function headline(
|
||||||
|
string $canonicalType,
|
||||||
|
OperationRun $run,
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
array $dominantCause,
|
||||||
|
array $context,
|
||||||
|
array $counts,
|
||||||
|
): string {
|
||||||
|
return match ($canonicalType) {
|
||||||
|
'baseline.capture' => $this->baselineCaptureHeadline($artifactTruth, $context, $counts, $operatorExplanation),
|
||||||
|
'baseline.compare' => $this->baselineCompareHeadline($artifactTruth, $context, $counts, $operatorExplanation),
|
||||||
|
'tenant.evidence.snapshot.generate' => $this->evidenceSnapshotHeadline($artifactTruth, $operatorExplanation),
|
||||||
|
'tenant.review.compose' => $this->reviewComposeHeadline($artifactTruth, $dominantCause, $operatorExplanation),
|
||||||
|
'tenant.review_pack.generate' => $this->reviewPackHeadline($artifactTruth, $dominantCause, $operatorExplanation),
|
||||||
|
default => $operatorExplanation?->headline
|
||||||
|
?? $artifactTruth?->primaryExplanation
|
||||||
|
?? 'This governance run needs review before it can be relied on.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
*/
|
||||||
|
private function baselineCaptureHeadline(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
array $context,
|
||||||
|
array $counts,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): string {
|
||||||
|
$subjectsTotal = $this->intValue(data_get($context, 'baseline_capture.subjects_total'));
|
||||||
|
$resumeToken = data_get($context, 'baseline_capture.resume_token');
|
||||||
|
$gapCount = $this->intValue(data_get($context, 'baseline_capture.gaps.count'));
|
||||||
|
|
||||||
|
if ($subjectsTotal === 0) {
|
||||||
|
return 'No baseline was captured because no governed subjects were ready.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($resumeToken) && trim($resumeToken) !== '') {
|
||||||
|
return 'The baseline capture started, but more evidence still needs to be collected.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gapCount > 0) {
|
||||||
|
return 'The baseline capture finished, but evidence gaps still limit the snapshot.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($artifactTruth?->artifactExistence ?? null) === 'created_but_not_usable') {
|
||||||
|
return 'The baseline capture finished without a usable snapshot.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($counts['total'] ?? 0) === 0 && ($counts['processed'] ?? 0) === 0) {
|
||||||
|
return 'The baseline capture finished without producing a decision-grade snapshot.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $operatorExplanation?->headline
|
||||||
|
?? $artifactTruth?->primaryExplanation
|
||||||
|
?? 'The baseline capture needs review before it can be used.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
*/
|
||||||
|
private function baselineCompareHeadline(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
array $context,
|
||||||
|
array $counts,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): string {
|
||||||
|
$reasonCode = (string) data_get($context, 'baseline_compare.reason_code', '');
|
||||||
|
$proof = data_get($context, 'baseline_compare.coverage.proof');
|
||||||
|
$resumeToken = data_get($context, 'baseline_compare.resume_token');
|
||||||
|
|
||||||
|
if ($reasonCode === BaselineCompareReasonCode::AmbiguousSubjects->value) {
|
||||||
|
return 'The compare finished, but ambiguous subject matching limited the result.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reasonCode === BaselineCompareReasonCode::StrategyFailed->value) {
|
||||||
|
return 'The compare finished, but a compare strategy failure kept the result incomplete.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($resumeToken) && trim($resumeToken) !== '') {
|
||||||
|
return 'The compare finished, but evidence capture still needs to resume before the result is complete.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($counts['total'] ?? 0) === 0 && ($counts['processed'] ?? 0) === 0) {
|
||||||
|
return 'The compare finished, but no decision-grade result is available yet.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($proof === false) {
|
||||||
|
return 'The compare finished, but missing coverage proof suppressed the normal result.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $operatorExplanation?->headline
|
||||||
|
?? $artifactTruth?->primaryExplanation
|
||||||
|
?? 'The compare needs follow-up before it can be treated as complete.';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function evidenceSnapshotHeadline(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): string {
|
||||||
|
return match (true) {
|
||||||
|
$artifactTruth?->freshnessState === 'stale' => 'The snapshot finished processing, but its evidence basis is already stale.',
|
||||||
|
$artifactTruth?->contentState === 'partial' => 'The snapshot finished processing, but its evidence basis is incomplete.',
|
||||||
|
$artifactTruth?->contentState === 'missing_input' => 'The snapshot finished processing without a complete evidence basis.',
|
||||||
|
default => $operatorExplanation?->headline
|
||||||
|
?? $artifactTruth?->primaryExplanation
|
||||||
|
?? 'The evidence snapshot needs review before it is relied on.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{code: ?string, label: string, explanation: string} $dominantCause
|
||||||
|
*/
|
||||||
|
private function reviewComposeHeadline(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
array $dominantCause,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): string {
|
||||||
|
return match (true) {
|
||||||
|
$artifactTruth?->contentState === 'partial' && $artifactTruth?->freshnessState === 'stale'
|
||||||
|
=> 'The review was generated, but missing sections and stale evidence keep it from being decision-grade.',
|
||||||
|
$artifactTruth?->contentState === 'partial'
|
||||||
|
=> 'The review was generated, but required sections are still incomplete.',
|
||||||
|
$artifactTruth?->freshnessState === 'stale'
|
||||||
|
=> 'The review was generated, but it relies on stale evidence.',
|
||||||
|
default => $operatorExplanation?->headline
|
||||||
|
?? $dominantCause['explanation']
|
||||||
|
?? 'The review needs follow-up before it should guide action.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{code: ?string, label: string, explanation: string} $dominantCause
|
||||||
|
*/
|
||||||
|
private function reviewPackHeadline(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
array $dominantCause,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): string {
|
||||||
|
return match (true) {
|
||||||
|
$artifactTruth?->publicationReadiness === 'blocked'
|
||||||
|
=> 'The pack did not produce a shareable artifact yet.',
|
||||||
|
$artifactTruth?->publicationReadiness === 'internal_only'
|
||||||
|
=> 'The pack finished, but it should stay internal until the source review is refreshed.',
|
||||||
|
default => $operatorExplanation?->headline
|
||||||
|
?? $dominantCause['explanation']
|
||||||
|
?? 'The review pack needs follow-up before it is shared.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{code: ?string, label: string, explanation: string} $dominantCause
|
||||||
|
*/
|
||||||
|
private function primaryReason(
|
||||||
|
array $dominantCause,
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
|
): string {
|
||||||
|
return $dominantCause['explanation']
|
||||||
|
?? $operatorExplanation?->dominantCauseExplanation
|
||||||
|
?? $reasonEnvelope?->shortExplanation
|
||||||
|
?? $artifactTruth?->primaryExplanation
|
||||||
|
?? $operatorExplanation?->reliabilityStatement
|
||||||
|
?? 'TenantPilot recorded diagnostic detail for this run.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
*/
|
||||||
|
private function nextActionCategory(
|
||||||
|
string $canonicalType,
|
||||||
|
OperationRun $run,
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
array $context,
|
||||||
|
): string {
|
||||||
|
if ($reasonEnvelope?->actionability === 'retryable_transient' || $operatorExplanation?->nextActionCategory === 'retry_later') {
|
||||||
|
return 'retry_later';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($canonicalType, ['baseline.capture', 'baseline.compare'], true)) {
|
||||||
|
$resumeToken = $canonicalType === 'baseline.capture'
|
||||||
|
? data_get($context, 'baseline_capture.resume_token')
|
||||||
|
: data_get($context, 'baseline_compare.resume_token');
|
||||||
|
|
||||||
|
if (is_string($resumeToken) && trim($resumeToken) !== '') {
|
||||||
|
return 'resume_capture_or_generation';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$reasonCode = (string) (data_get($context, 'baseline_compare.reason_code') ?? $reasonEnvelope?->internalCode ?? '');
|
||||||
|
|
||||||
|
if (in_array($reasonCode, [
|
||||||
|
BaselineCompareReasonCode::AmbiguousSubjects->value,
|
||||||
|
BaselineCompareReasonCode::UnsupportedSubjects->value,
|
||||||
|
], true)) {
|
||||||
|
return 'review_scope_or_ambiguous_matches';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($canonicalType === 'baseline.capture' && $this->intValue(data_get($context, 'baseline_capture.subjects_total')) === 0) {
|
||||||
|
return 'refresh_prerequisite_data';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($operatorExplanation?->nextActionCategory === 'none' || trim((string) $operatorExplanation?->nextActionText) === 'No action needed') {
|
||||||
|
return 'no_further_action';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$reasonEnvelope?->actionability === 'prerequisite_missing'
|
||||||
|
|| in_array($canonicalType, ['tenant.evidence.snapshot.generate', 'tenant.review.compose', 'tenant.review_pack.generate'], true)
|
||||||
|
) {
|
||||||
|
return 'refresh_prerequisite_data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'manually_validate';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nextActionText(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
|
): string {
|
||||||
|
$text = $operatorExplanation?->nextActionText
|
||||||
|
?? $artifactTruth?->nextStepText()
|
||||||
|
?? $reasonEnvelope?->firstNextStep()?->label
|
||||||
|
?? 'No action needed';
|
||||||
|
|
||||||
|
return trim(rtrim($text, '.')).'.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function affectedScaleCue(
|
||||||
|
string $canonicalType,
|
||||||
|
OperationRun $run,
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
array $context,
|
||||||
|
array $counts,
|
||||||
|
): ?array {
|
||||||
|
return match ($canonicalType) {
|
||||||
|
'baseline.capture' => $this->baselineCaptureScaleCue($context, $counts),
|
||||||
|
'baseline.compare' => $this->baselineCompareScaleCue($context, $counts),
|
||||||
|
'tenant.evidence.snapshot.generate' => $this->countDescriptorScaleCue($operatorExplanation?->countDescriptors ?? [], ['Missing dimensions', 'Stale dimensions', 'Evidence dimensions']),
|
||||||
|
'tenant.review.compose' => $this->reviewScaleCue($artifactTruth, $operatorExplanation?->countDescriptors ?? []),
|
||||||
|
'tenant.review_pack.generate' => $this->reviewPackScaleCue($artifactTruth, $operatorExplanation?->countDescriptors ?? []),
|
||||||
|
default => $this->summaryCountsScaleCue($counts),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function baselineCaptureScaleCue(array $context, array $counts): ?array
|
||||||
|
{
|
||||||
|
$subjectsTotal = $this->intValue(data_get($context, 'baseline_capture.subjects_total'));
|
||||||
|
$gapCount = $this->intValue(data_get($context, 'baseline_capture.gaps.count'));
|
||||||
|
|
||||||
|
if ($gapCount > 0) {
|
||||||
|
return [
|
||||||
|
'label' => 'Affected subjects',
|
||||||
|
'value' => "{$gapCount} governed subjects still need evidence follow-up.",
|
||||||
|
'source' => 'context',
|
||||||
|
'confidence' => 'exact',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subjectsTotal >= 0) {
|
||||||
|
return [
|
||||||
|
'label' => 'Capture scope',
|
||||||
|
'value' => "{$subjectsTotal} governed subjects were in the recorded capture scope.",
|
||||||
|
'source' => 'context',
|
||||||
|
'confidence' => 'exact',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->summaryCountsScaleCue($counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function baselineCompareScaleCue(array $context, array $counts): ?array
|
||||||
|
{
|
||||||
|
$gapCount = $this->intValue(data_get($context, 'baseline_compare.evidence_gaps.count'));
|
||||||
|
$subjectsTotal = $this->intValue(data_get($context, 'baseline_compare.subjects_total'));
|
||||||
|
$uncoveredTypes = array_values(array_filter((array) data_get($context, 'baseline_compare.coverage.uncovered_types', []), 'is_string'));
|
||||||
|
|
||||||
|
if ($gapCount > 0) {
|
||||||
|
return [
|
||||||
|
'label' => 'Affected subjects',
|
||||||
|
'value' => "{$gapCount} governed subjects still have evidence gaps.",
|
||||||
|
'source' => 'context',
|
||||||
|
'confidence' => 'exact',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uncoveredTypes !== []) {
|
||||||
|
$count = count($uncoveredTypes);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Coverage scope',
|
||||||
|
'value' => "{$count} policy types were left without proven compare coverage.",
|
||||||
|
'source' => 'context',
|
||||||
|
'confidence' => 'bounded',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($subjectsTotal > 0) {
|
||||||
|
return [
|
||||||
|
'label' => 'Compare scope',
|
||||||
|
'value' => "{$subjectsTotal} governed subjects were in scope for this compare run.",
|
||||||
|
'source' => 'context',
|
||||||
|
'confidence' => 'exact',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->summaryCountsScaleCue($counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, CountDescriptor> $countDescriptors
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function reviewScaleCue(?ArtifactTruthEnvelope $artifactTruth, array $countDescriptors): ?array
|
||||||
|
{
|
||||||
|
if ($artifactTruth?->contentState === 'partial') {
|
||||||
|
$sections = $this->findCountDescriptor($countDescriptors, 'Sections');
|
||||||
|
|
||||||
|
if ($sections instanceof CountDescriptor) {
|
||||||
|
return [
|
||||||
|
'label' => 'Review sections',
|
||||||
|
'value' => "{$sections->value} sections were recorded and still need review for completeness.",
|
||||||
|
'source' => 'related_artifact_truth',
|
||||||
|
'confidence' => 'best_available',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => 'Review sections',
|
||||||
|
'value' => 'Required review sections are still incomplete.',
|
||||||
|
'source' => 'related_artifact_truth',
|
||||||
|
'confidence' => 'best_available',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->freshnessState === 'stale') {
|
||||||
|
return [
|
||||||
|
'label' => 'Evidence freshness',
|
||||||
|
'value' => 'The source evidence is stale for at least part of this review.',
|
||||||
|
'source' => 'related_artifact_truth',
|
||||||
|
'confidence' => 'best_available',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->countDescriptorScaleCue($countDescriptors, ['Sections', 'Findings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, CountDescriptor> $countDescriptors
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function reviewPackScaleCue(?ArtifactTruthEnvelope $artifactTruth, array $countDescriptors): ?array
|
||||||
|
{
|
||||||
|
if ($artifactTruth?->publicationReadiness === 'internal_only') {
|
||||||
|
return [
|
||||||
|
'label' => 'Sharing scope',
|
||||||
|
'value' => 'The pack is suitable for internal follow-up only in its current state.',
|
||||||
|
'source' => 'related_artifact_truth',
|
||||||
|
'confidence' => 'best_available',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->countDescriptorScaleCue($countDescriptors, ['Reports', 'Findings']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, CountDescriptor> $countDescriptors
|
||||||
|
* @param list<string> $preferredLabels
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function countDescriptorScaleCue(array $countDescriptors, array $preferredLabels): ?array
|
||||||
|
{
|
||||||
|
foreach ($preferredLabels as $label) {
|
||||||
|
$descriptor = $this->findCountDescriptor($countDescriptors, $label);
|
||||||
|
|
||||||
|
if (! $descriptor instanceof CountDescriptor || $descriptor->value <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $descriptor->label,
|
||||||
|
'value' => "{$descriptor->value} {$this->pluralizeDescriptor($descriptor)}.",
|
||||||
|
'source' => 'related_artifact_truth',
|
||||||
|
'confidence' => 'exact',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, int> $counts
|
||||||
|
* @return array{label: string, value: string, source: string, confidence?: string}|null
|
||||||
|
*/
|
||||||
|
private function summaryCountsScaleCue(array $counts): ?array
|
||||||
|
{
|
||||||
|
foreach (['total', 'processed', 'failed', 'items', 'finding_count'] as $key) {
|
||||||
|
$value = (int) ($counts[$key] ?? 0);
|
||||||
|
|
||||||
|
if ($value <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => SummaryCountsNormalizer::label($key),
|
||||||
|
'value' => "{$value} recorded in the canonical run counters.",
|
||||||
|
'source' => 'summary_counts',
|
||||||
|
'confidence' => 'exact',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @return list<array{rank: int, code: ?string, label: string, explanation: string}>
|
||||||
|
*/
|
||||||
|
private function rankCauseCandidates(
|
||||||
|
string $canonicalType,
|
||||||
|
OperationRun $run,
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
|
array $context,
|
||||||
|
): array {
|
||||||
|
$candidates = [];
|
||||||
|
|
||||||
|
$this->pushCandidate(
|
||||||
|
$candidates,
|
||||||
|
code: $reasonEnvelope?->internalCode ?? $operatorExplanation?->dominantCauseCode,
|
||||||
|
label: $reasonEnvelope?->operatorLabel ?? $operatorExplanation?->dominantCauseLabel ?? $artifactTruth?->primaryLabel,
|
||||||
|
explanation: $reasonEnvelope?->shortExplanation ?? $operatorExplanation?->dominantCauseExplanation ?? $artifactTruth?->primaryExplanation,
|
||||||
|
rank: $this->reasonRank($reasonEnvelope, $operatorExplanation),
|
||||||
|
);
|
||||||
|
|
||||||
|
match ($canonicalType) {
|
||||||
|
'baseline.capture' => $this->baselineCaptureCandidates($candidates, $context),
|
||||||
|
'baseline.compare' => $this->baselineCompareCandidates($candidates, $context),
|
||||||
|
'tenant.evidence.snapshot.generate' => $this->evidenceSnapshotCandidates($candidates, $artifactTruth, $operatorExplanation),
|
||||||
|
'tenant.review.compose' => $this->reviewComposeCandidates($candidates, $artifactTruth),
|
||||||
|
'tenant.review_pack.generate' => $this->reviewPackCandidates($candidates, $artifactTruth),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
usort($candidates, static function (array $left, array $right): int {
|
||||||
|
$rank = ($right['rank'] <=> $left['rank']);
|
||||||
|
|
||||||
|
if ($rank !== 0) {
|
||||||
|
return $rank;
|
||||||
|
}
|
||||||
|
|
||||||
|
return strcmp($left['label'], $right['label']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_values(array_map(
|
||||||
|
static fn (array $candidate): array => [
|
||||||
|
'code' => $candidate['code'],
|
||||||
|
'label' => $candidate['label'],
|
||||||
|
'explanation' => $candidate['explanation'],
|
||||||
|
],
|
||||||
|
$candidates,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string, rank: int}> $candidates
|
||||||
|
*/
|
||||||
|
private function pushCandidate(array &$candidates, ?string $code, ?string $label, ?string $explanation, int $rank): void
|
||||||
|
{
|
||||||
|
$label = is_string($label) ? trim($label) : '';
|
||||||
|
$explanation = is_string($explanation) ? trim($explanation) : '';
|
||||||
|
|
||||||
|
if ($label === '' || $explanation === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($candidates as $candidate) {
|
||||||
|
if (($candidate['label'] ?? null) === $label && ($candidate['explanation'] ?? null) === $explanation) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates[] = [
|
||||||
|
'code' => $code,
|
||||||
|
'label' => $label,
|
||||||
|
'explanation' => $explanation,
|
||||||
|
'rank' => $rank,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string, rank: int}> $candidates
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
*/
|
||||||
|
private function baselineCaptureCandidates(array &$candidates, array $context): void
|
||||||
|
{
|
||||||
|
$subjectsTotal = $this->intValue(data_get($context, 'baseline_capture.subjects_total'));
|
||||||
|
$gapCount = $this->intValue(data_get($context, 'baseline_capture.gaps.count'));
|
||||||
|
$resumeToken = data_get($context, 'baseline_capture.resume_token');
|
||||||
|
|
||||||
|
if ($subjectsTotal === 0) {
|
||||||
|
$this->pushCandidate($candidates, 'no_subjects_in_scope', 'No governed subjects captured', 'No governed subjects were available for this baseline capture.', 95);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gapCount > 0) {
|
||||||
|
$this->pushCandidate($candidates, 'baseline_capture_gaps', 'Evidence gaps remain', "{$gapCount} governed subjects still need evidence capture before the snapshot is complete.", 82);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($resumeToken) && trim($resumeToken) !== '') {
|
||||||
|
$this->pushCandidate($candidates, 'baseline_capture_resume', 'Capture can resume', 'TenantPilot recorded a resume point because this capture could not finish in one pass.', 84);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string, rank: int}> $candidates
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
*/
|
||||||
|
private function baselineCompareCandidates(array &$candidates, array $context): void
|
||||||
|
{
|
||||||
|
$reasonCode = (string) data_get($context, 'baseline_compare.reason_code', '');
|
||||||
|
$gapCount = $this->intValue(data_get($context, 'baseline_compare.evidence_gaps.count'));
|
||||||
|
$uncoveredTypes = array_values(array_filter((array) data_get($context, 'baseline_compare.coverage.uncovered_types', []), 'is_string'));
|
||||||
|
$proof = data_get($context, 'baseline_compare.coverage.proof');
|
||||||
|
$resumeToken = data_get($context, 'baseline_compare.resume_token');
|
||||||
|
|
||||||
|
if ($reasonCode === BaselineCompareReasonCode::AmbiguousSubjects->value) {
|
||||||
|
$this->pushCandidate($candidates, $reasonCode, 'Ambiguous matches', 'One or more governed subjects stayed ambiguous, so the compare result needs scope review.', 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reasonCode === BaselineCompareReasonCode::StrategyFailed->value) {
|
||||||
|
$this->pushCandidate($candidates, $reasonCode, 'Compare strategy failed', 'A compare strategy failed while processing in-scope governed subjects.', 94);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($gapCount > 0) {
|
||||||
|
$this->pushCandidate($candidates, 'baseline_compare_gaps', 'Evidence gaps', "{$gapCount} governed subjects still have evidence gaps, so the compare output is incomplete.", 83);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($proof === false || $uncoveredTypes !== []) {
|
||||||
|
$count = count($uncoveredTypes);
|
||||||
|
$explanation = $count > 0
|
||||||
|
? "{$count} policy types were left without proven compare coverage."
|
||||||
|
: 'Coverage proof was missing, so TenantPilot suppressed part of the normal compare output.';
|
||||||
|
|
||||||
|
$this->pushCandidate($candidates, 'coverage_unproven', 'Coverage proof missing', $explanation, 81);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($resumeToken) && trim($resumeToken) !== '') {
|
||||||
|
$this->pushCandidate($candidates, 'baseline_compare_resume', 'Evidence capture needs to resume', 'The compare recorded a resume point because evidence capture did not finish in one pass.', 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string, rank: int}> $candidates
|
||||||
|
* @param array<int, CountDescriptor> $countDescriptors
|
||||||
|
*/
|
||||||
|
private function evidenceSnapshotCandidates(array &$candidates, ?ArtifactTruthEnvelope $artifactTruth, ?OperatorExplanationPattern $operatorExplanation): void
|
||||||
|
{
|
||||||
|
$countDescriptors = $operatorExplanation?->countDescriptors ?? [];
|
||||||
|
$missing = $this->findCountDescriptor($countDescriptors, 'Missing dimensions');
|
||||||
|
$stale = $this->findCountDescriptor($countDescriptors, 'Stale dimensions');
|
||||||
|
|
||||||
|
if ($missing instanceof CountDescriptor && $missing->value > 0) {
|
||||||
|
$this->pushCandidate($candidates, 'missing_dimensions', 'Missing dimensions', "{$missing->value} evidence dimensions are still missing from this snapshot.", 88);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->freshnessState === 'stale' || ($stale instanceof CountDescriptor && $stale->value > 0)) {
|
||||||
|
$value = $stale instanceof CountDescriptor && $stale->value > 0
|
||||||
|
? "{$stale->value} evidence dimensions are stale and should be refreshed."
|
||||||
|
: 'Part of the evidence basis is stale and should be refreshed before use.';
|
||||||
|
|
||||||
|
$this->pushCandidate($candidates, 'stale_evidence', 'Stale evidence basis', $value, 82);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string, rank: int}> $candidates
|
||||||
|
*/
|
||||||
|
private function reviewComposeCandidates(array &$candidates, ?ArtifactTruthEnvelope $artifactTruth): void
|
||||||
|
{
|
||||||
|
if ($artifactTruth?->contentState === 'partial') {
|
||||||
|
$this->pushCandidate($candidates, 'review_missing_sections', 'Missing sections', 'Required review sections are still incomplete for this generated review.', 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->freshnessState === 'stale') {
|
||||||
|
$this->pushCandidate($candidates, 'review_stale_evidence', 'Stale evidence basis', 'The review relies on stale evidence and needs a refreshed evidence basis.', 86);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->publicationReadiness === 'blocked') {
|
||||||
|
$this->pushCandidate($candidates, 'review_blocked', 'Publication blocked', 'The review cannot move forward until its blocking prerequisites are cleared.', 95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string, rank: int}> $candidates
|
||||||
|
*/
|
||||||
|
private function reviewPackCandidates(array &$candidates, ?ArtifactTruthEnvelope $artifactTruth): void
|
||||||
|
{
|
||||||
|
if ($artifactTruth?->publicationReadiness === 'blocked') {
|
||||||
|
$this->pushCandidate($candidates, 'review_pack_blocked', 'Shareable pack not available', 'The pack did not produce a shareable artifact yet.', 94);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->publicationReadiness === 'internal_only') {
|
||||||
|
$this->pushCandidate($candidates, 'review_pack_internal_only', 'Internal-only outcome', 'The pack can support internal follow-up, but it should not be shared externally yet.', 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->freshnessState === 'stale') {
|
||||||
|
$this->pushCandidate($candidates, 'review_pack_stale_source', 'Source review is stale', 'The pack inherits stale review evidence and needs a refreshed source review.', 84);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->contentState === 'partial') {
|
||||||
|
$this->pushCandidate($candidates, 'review_pack_partial_source', 'Source review is incomplete', 'The pack inherits incomplete source review content and needs follow-up before sharing.', 86);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reasonRank(
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
): int {
|
||||||
|
if ($reasonEnvelope?->actionability === 'retryable_transient') {
|
||||||
|
return 76;
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($operatorExplanation?->nextActionCategory) {
|
||||||
|
'fix_prerequisite' => 92,
|
||||||
|
'retry_later' => 76,
|
||||||
|
'none' => 40,
|
||||||
|
default => 85,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{code: ?string, label: string, explanation: string}> $secondaryCauses
|
||||||
|
* @return list<array{
|
||||||
|
* label: string,
|
||||||
|
* value: string,
|
||||||
|
* hint?: ?string,
|
||||||
|
* emphasis?: string
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
private function secondaryFacts(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
array $secondaryCauses,
|
||||||
|
string $nextActionCategory,
|
||||||
|
string $nextActionText,
|
||||||
|
): array {
|
||||||
|
$facts = [];
|
||||||
|
|
||||||
|
if ($operatorExplanation instanceof OperatorExplanationPattern) {
|
||||||
|
$facts[] = [
|
||||||
|
'label' => 'Result trust',
|
||||||
|
'value' => $operatorExplanation->trustworthinessLabel(),
|
||||||
|
'hint' => $this->deduplicateSecondaryFactHint(
|
||||||
|
$operatorExplanation->reliabilityStatement,
|
||||||
|
$operatorExplanation->dominantCauseExplanation,
|
||||||
|
$artifactTruth?->primaryExplanation,
|
||||||
|
),
|
||||||
|
'emphasis' => $this->emphasisFromTrust($operatorExplanation->trustworthinessLevel->value),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($operatorExplanation->evaluationResultLabel() !== '') {
|
||||||
|
$facts[] = [
|
||||||
|
'label' => 'Result meaning',
|
||||||
|
'value' => $operatorExplanation->evaluationResultLabel(),
|
||||||
|
'hint' => $operatorExplanation->coverageStatement,
|
||||||
|
'emphasis' => 'neutral',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($secondaryCauses !== []) {
|
||||||
|
$facts[] = [
|
||||||
|
'label' => 'Secondary causes',
|
||||||
|
'value' => implode(' · ', array_map(static fn (array $cause): string => $cause['label'], $secondaryCauses)),
|
||||||
|
'hint' => 'Additional contributing causes stay visible without replacing the dominant cause.',
|
||||||
|
'emphasis' => 'caution',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($artifactTruth?->relatedArtifactUrl === null && $nextActionCategory !== 'no_further_action') {
|
||||||
|
$facts[] = [
|
||||||
|
'label' => 'Related artifact access',
|
||||||
|
'value' => 'No related artifact link is available from this run.',
|
||||||
|
'emphasis' => 'neutral',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $facts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function emphasisFromTrust(string $trust): string
|
||||||
|
{
|
||||||
|
return match ($trust) {
|
||||||
|
'unusable' => 'blocked',
|
||||||
|
'diagnostic_only', 'limited_confidence' => 'caution',
|
||||||
|
default => 'neutral',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deduplicateSecondaryFactHint(?string $hint, ?string ...$duplicates): ?string
|
||||||
|
{
|
||||||
|
$normalizedHint = $this->normalizeFactText($hint);
|
||||||
|
|
||||||
|
if ($normalizedHint === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($duplicates as $duplicate) {
|
||||||
|
if ($normalizedHint === $this->normalizeFactText($duplicate)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim($hint ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fallbackCause(
|
||||||
|
?ArtifactTruthEnvelope $artifactTruth,
|
||||||
|
?OperatorExplanationPattern $operatorExplanation,
|
||||||
|
?ReasonResolutionEnvelope $reasonEnvelope,
|
||||||
|
): array {
|
||||||
|
return [
|
||||||
|
'code' => $reasonEnvelope?->internalCode ?? $operatorExplanation?->dominantCauseCode,
|
||||||
|
'label' => $reasonEnvelope?->operatorLabel
|
||||||
|
?? $operatorExplanation?->dominantCauseLabel
|
||||||
|
?? $artifactTruth?->primaryLabel
|
||||||
|
?? 'Follow-up required',
|
||||||
|
'explanation' => $reasonEnvelope?->shortExplanation
|
||||||
|
?? $operatorExplanation?->dominantCauseExplanation
|
||||||
|
?? $artifactTruth?->primaryExplanation
|
||||||
|
?? 'TenantPilot recorded enough detail to keep this run out of an all-clear state.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findCountDescriptor(array $countDescriptors, string $label): ?CountDescriptor
|
||||||
|
{
|
||||||
|
foreach ($countDescriptors as $descriptor) {
|
||||||
|
if ($descriptor instanceof CountDescriptor && $descriptor->label === $label) {
|
||||||
|
return $descriptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function intValue(mixed $value): ?int
|
||||||
|
{
|
||||||
|
return is_numeric($value) ? (int) $value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function pluralizeDescriptor(CountDescriptor $descriptor): string
|
||||||
|
{
|
||||||
|
return match ($descriptor->label) {
|
||||||
|
'Missing dimensions' => 'evidence dimensions are missing',
|
||||||
|
'Stale dimensions' => 'evidence dimensions are stale',
|
||||||
|
'Evidence dimensions' => 'evidence dimensions were recorded',
|
||||||
|
'Sections' => 'sections were recorded',
|
||||||
|
'Reports' => 'reports were recorded',
|
||||||
|
'Findings' => 'findings were recorded',
|
||||||
|
default => strtolower($descriptor->label).' were recorded',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeFactText(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = trim((string) preg_replace('/\s+/', ' ', $value));
|
||||||
|
|
||||||
|
if ($normalized === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb_strtolower($normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -350,6 +350,16 @@ public static function governanceOperatorExplanation(OperationRun $run): ?Operat
|
|||||||
return self::resolveGovernanceOperatorExplanation($run);
|
return self::resolveGovernanceOperatorExplanation($run);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function governanceDiagnosticSummary(OperationRun $run): ?GovernanceRunDiagnosticSummary
|
||||||
|
{
|
||||||
|
return self::resolveGovernanceDiagnosticSummary($run);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function governanceDiagnosticSummaryFresh(OperationRun $run): ?GovernanceRunDiagnosticSummary
|
||||||
|
{
|
||||||
|
return self::resolveGovernanceDiagnosticSummary($run, fresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
public static function governanceOperatorExplanationFresh(OperationRun $run): ?OperatorExplanationPattern
|
public static function governanceOperatorExplanationFresh(OperationRun $run): ?OperatorExplanationPattern
|
||||||
{
|
{
|
||||||
return self::resolveGovernanceOperatorExplanation($run, fresh: true);
|
return self::resolveGovernanceOperatorExplanation($run, fresh: true);
|
||||||
@ -492,6 +502,29 @@ private static function resolveGovernanceOperatorExplanation(OperationRun $run,
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function resolveGovernanceDiagnosticSummary(OperationRun $run, bool $fresh = false): ?GovernanceRunDiagnosticSummary
|
||||||
|
{
|
||||||
|
if (! $run->supportsOperatorExplanation()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::memoizeExplanation(
|
||||||
|
run: $run,
|
||||||
|
variant: 'governance_diagnostic_summary',
|
||||||
|
resolver: fn (): ?GovernanceRunDiagnosticSummary => app(GovernanceRunDiagnosticSummaryBuilder::class)->build(
|
||||||
|
run: $run,
|
||||||
|
artifactTruth: $fresh
|
||||||
|
? app(ArtifactTruthPresenter::class)->forOperationRunFresh($run)
|
||||||
|
: app(ArtifactTruthPresenter::class)->forOperationRun($run),
|
||||||
|
operatorExplanation: $fresh
|
||||||
|
? self::resolveGovernanceOperatorExplanation($run, fresh: true)
|
||||||
|
: self::resolveGovernanceOperatorExplanation($run),
|
||||||
|
reasonEnvelope: app(ReasonPresenter::class)->forOperationRun($run, 'run_detail'),
|
||||||
|
),
|
||||||
|
fresh: $fresh,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private static function memoizeGuidance(
|
private static function memoizeGuidance(
|
||||||
OperationRun $run,
|
OperationRun $run,
|
||||||
string $variant,
|
string $variant,
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public function __construct(
|
|||||||
public ?string $operatorLabel,
|
public ?string $operatorLabel,
|
||||||
public ?string $shortExplanation,
|
public ?string $shortExplanation,
|
||||||
public ?string $diagnosticCode,
|
public ?string $diagnosticCode,
|
||||||
|
public ?string $actionability,
|
||||||
public string $trustImpact,
|
public string $trustImpact,
|
||||||
public ?string $absencePattern,
|
public ?string $absencePattern,
|
||||||
public array $nextSteps = [],
|
public array $nextSteps = [],
|
||||||
@ -43,6 +44,7 @@ public static function fromReasonResolutionEnvelope(
|
|||||||
operatorLabel: $reason->operatorLabel,
|
operatorLabel: $reason->operatorLabel,
|
||||||
shortExplanation: $reason->shortExplanation,
|
shortExplanation: $reason->shortExplanation,
|
||||||
diagnosticCode: $reason->diagnosticCode(),
|
diagnosticCode: $reason->diagnosticCode(),
|
||||||
|
actionability: $reason->actionability,
|
||||||
trustImpact: $reason->trustImpact,
|
trustImpact: $reason->trustImpact,
|
||||||
absencePattern: $reason->absencePattern,
|
absencePattern: $reason->absencePattern,
|
||||||
nextSteps: array_values(array_map(
|
nextSteps: array_values(array_map(
|
||||||
@ -79,7 +81,8 @@ public function toReasonResolutionEnvelope(): ReasonResolutionEnvelope
|
|||||||
internalCode: $this->reasonCode ?? 'artifact_truth_reason',
|
internalCode: $this->reasonCode ?? 'artifact_truth_reason',
|
||||||
operatorLabel: $this->operatorLabel ?? 'Operator attention required',
|
operatorLabel: $this->operatorLabel ?? 'Operator attention required',
|
||||||
shortExplanation: $this->shortExplanation ?? 'Technical diagnostics are available for this result.',
|
shortExplanation: $this->shortExplanation ?? 'Technical diagnostics are available for this result.',
|
||||||
actionability: $this->absencePattern === 'true_no_result' ? 'non_actionable' : 'prerequisite_missing',
|
actionability: $this->actionability
|
||||||
|
?? ($this->absencePattern === 'true_no_result' ? 'non_actionable' : 'prerequisite_missing'),
|
||||||
nextSteps: array_map(
|
nextSteps: array_map(
|
||||||
static fn (string $label): NextStepOption => NextStepOption::instruction($label),
|
static fn (string $label): NextStepOption => NextStepOption::instruction($label),
|
||||||
$this->nextSteps,
|
$this->nextSteps,
|
||||||
@ -98,6 +101,7 @@ public function toReasonResolutionEnvelope(): ReasonResolutionEnvelope
|
|||||||
* operatorLabel: ?string,
|
* operatorLabel: ?string,
|
||||||
* shortExplanation: ?string,
|
* shortExplanation: ?string,
|
||||||
* diagnosticCode: ?string,
|
* diagnosticCode: ?string,
|
||||||
|
* actionability: ?string,
|
||||||
* trustImpact: string,
|
* trustImpact: string,
|
||||||
* absencePattern: ?string,
|
* absencePattern: ?string,
|
||||||
* nextSteps: array<int, string>,
|
* nextSteps: array<int, string>,
|
||||||
@ -114,6 +118,7 @@ public function toArray(): array
|
|||||||
'operatorLabel' => $this->operatorLabel,
|
'operatorLabel' => $this->operatorLabel,
|
||||||
'shortExplanation' => $this->shortExplanation,
|
'shortExplanation' => $this->shortExplanation,
|
||||||
'diagnosticCode' => $this->diagnosticCode,
|
'diagnosticCode' => $this->diagnosticCode,
|
||||||
|
'actionability' => $this->actionability,
|
||||||
'trustImpact' => $this->trustImpact,
|
'trustImpact' => $this->trustImpact,
|
||||||
'absencePattern' => $this->absencePattern,
|
'absencePattern' => $this->absencePattern,
|
||||||
'nextSteps' => $this->nextSteps,
|
'nextSteps' => $this->nextSteps,
|
||||||
|
|||||||
@ -11,44 +11,6 @@
|
|||||||
<div
|
<div
|
||||||
@if ($pollInterval !== null) wire:poll.{{ $pollInterval }} @endif
|
@if ($pollInterval !== null) wire:poll.{{ $pollInterval }} @endif
|
||||||
>
|
>
|
||||||
<x-filament::section heading="Monitoring detail" class="mb-6">
|
|
||||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
Scope context, return navigation, utility, related drilldowns, and run-specific follow-up stay in separate lanes on this viewer.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope context</p>
|
|
||||||
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['scope_label'] }}</p>
|
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['scope_body'] }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Navigation lane</p>
|
|
||||||
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['navigation_label'] }}</p>
|
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['navigation_body'] }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Utility lane</p>
|
|
||||||
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Refresh</p>
|
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['utility_body'] }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Related drilldown</p>
|
|
||||||
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Open</p>
|
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['related_body'] }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Follow-up lane</p>
|
|
||||||
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['follow_up_label'] ?? 'No follow-up action' }}</p>
|
|
||||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['follow_up_body'] }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</x-filament::section>
|
|
||||||
|
|
||||||
@if ($contextBanner !== null)
|
@if ($contextBanner !== null)
|
||||||
@php
|
@php
|
||||||
$bannerClasses = match ($contextBanner['tone']) {
|
$bannerClasses = match ($contextBanner['tone']) {
|
||||||
@ -117,5 +79,43 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
{{ $this->infolist }}
|
{{ $this->infolist }}
|
||||||
|
|
||||||
|
<x-filament::section heading="Monitoring detail" class="mt-6">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Scope context, return navigation, utility, related drilldowns, and run-specific follow-up stay in separate lanes on this viewer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Scope context</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['scope_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['scope_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Navigation lane</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['navigation_label'] }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['navigation_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Utility lane</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Refresh</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['utility_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Related drilldown</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">Open</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['related_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-xl border border-gray-200 bg-white/80 p-4 dark:border-white/10 dark:bg-white/5">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.2em] text-gray-500 dark:text-gray-400">Follow-up lane</p>
|
||||||
|
<p class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">{{ $monitoringDetail['follow_up_label'] ?? 'No follow-up action' }}</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">{{ $monitoringDetail['follow_up_body'] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
</div>
|
</div>
|
||||||
</x-filament-panels::page>
|
</x-filament-panels::page>
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Filament\Pages\BaselineCompareLanding;
|
use App\Filament\Pages\BaselineCompareLanding;
|
||||||
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
use App\Filament\Resources\BaselineSnapshotResource;
|
use App\Filament\Resources\BaselineSnapshotResource;
|
||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
@ -15,6 +16,8 @@
|
|||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
use App\Services\Auth\WorkspaceCapabilityResolver;
|
use App\Services\Auth\WorkspaceCapabilityResolver;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
|
||||||
it('returns 404 for non-members on the baseline compare explanation surface', function (): void {
|
it('returns 404 for non-members on the baseline compare explanation surface', function (): void {
|
||||||
[$member, $tenant] = createUserWithTenant(role: 'owner');
|
[$member, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
@ -99,3 +102,65 @@
|
|||||||
->get(ReviewRegister::getUrl(panel: 'admin'))
|
->get(ReviewRegister::getUrl(panel: 'admin'))
|
||||||
->assertNotFound();
|
->assertNotFound();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders governance summary facts for entitled viewers on the canonical run detail surface', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'partially_succeeded',
|
||||||
|
'context' => [
|
||||||
|
'baseline_compare' => [
|
||||||
|
'reason_code' => 'ambiguous_subjects',
|
||||||
|
'evidence_gaps' => [
|
||||||
|
'count' => 2,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
$this->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
||||||
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||||
|
->assertSee('Artifact impact')
|
||||||
|
->assertSee('Dominant cause')
|
||||||
|
->assertSee('Ambiguous matches');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps governance summary surfaces deny-as-not-found for workspace members without tenant entitlement', function (): void {
|
||||||
|
$workspace = Workspace::factory()->create();
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'status' => 'active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'user_id' => (int) $user->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'type' => 'tenant.review_pack.generate',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'succeeded',
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
|
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
||||||
|
->assertNotFound();
|
||||||
|
});
|
||||||
|
|||||||
@ -79,6 +79,24 @@ protected function makePartialArtifactTruthEvidenceSnapshot(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function makeMissingArtifactTruthEvidenceSnapshot(
|
||||||
|
Tenant $tenant,
|
||||||
|
array $snapshotOverrides = [],
|
||||||
|
array $summaryOverrides = [],
|
||||||
|
): EvidenceSnapshot {
|
||||||
|
$snapshot = $this->makeArtifactTruthEvidenceSnapshot($tenant, $snapshotOverrides, $summaryOverrides);
|
||||||
|
|
||||||
|
return $this->restateArtifactTruthEvidenceSnapshot(
|
||||||
|
$snapshot,
|
||||||
|
EvidenceCompletenessState::Missing,
|
||||||
|
array_replace([
|
||||||
|
'dimension_count' => 0,
|
||||||
|
'missing_dimensions' => 1,
|
||||||
|
'stale_dimensions' => 0,
|
||||||
|
], $summaryOverrides),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected function makeArtifactTruthReview(
|
protected function makeArtifactTruthReview(
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
User $user,
|
User $user,
|
||||||
@ -115,6 +133,32 @@ protected function makeArtifactTruthReview(
|
|||||||
return TenantReview::query()->create(array_replace($defaults, $reviewOverrides));
|
return TenantReview::query()->create(array_replace($defaults, $reviewOverrides));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function makePartialArtifactTruthReview(
|
||||||
|
Tenant $tenant,
|
||||||
|
User $user,
|
||||||
|
?EvidenceSnapshot $snapshot = null,
|
||||||
|
array $reviewOverrides = [],
|
||||||
|
array $summaryOverrides = [],
|
||||||
|
): TenantReview {
|
||||||
|
return $this->makeArtifactTruthReview(
|
||||||
|
tenant: $tenant,
|
||||||
|
user: $user,
|
||||||
|
snapshot: $snapshot,
|
||||||
|
reviewOverrides: array_replace([
|
||||||
|
'status' => TenantReviewStatus::Ready->value,
|
||||||
|
'completeness_state' => TenantReviewCompletenessState::Partial->value,
|
||||||
|
], $reviewOverrides),
|
||||||
|
summaryOverrides: array_replace_recursive([
|
||||||
|
'section_state_counts' => [
|
||||||
|
'complete' => 4,
|
||||||
|
'partial' => 1,
|
||||||
|
'missing' => 1,
|
||||||
|
'stale' => 0,
|
||||||
|
],
|
||||||
|
], $summaryOverrides),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
protected function makeBlockedArtifactTruthReview(
|
protected function makeBlockedArtifactTruthReview(
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
User $user,
|
User $user,
|
||||||
|
|||||||
@ -77,7 +77,7 @@ function visibleLivewireText(Testable $component): string
|
|||||||
->assertSee('Outcome')
|
->assertSee('Outcome')
|
||||||
->assertSee('Artifact truth')
|
->assertSee('Artifact truth')
|
||||||
->assertSee('Execution failed')
|
->assertSee('Execution failed')
|
||||||
->assertSee($explanation?->headline ?? '')
|
->assertSee('The baseline capture finished without a usable snapshot.')
|
||||||
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
||||||
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
||||||
->assertSee('Artifact not usable')
|
->assertSee('Artifact not usable')
|
||||||
@ -136,11 +136,10 @@ function visibleLivewireText(Testable $component): string
|
|||||||
->assertSee('Result trust')
|
->assertSee('Result trust')
|
||||||
->assertSee('Primary next step')
|
->assertSee('Primary next step')
|
||||||
->assertSee('Artifact truth details')
|
->assertSee('Artifact truth details')
|
||||||
->assertSee($explanation?->headline ?? '')
|
->assertSee('The compare finished, but no decision-grade result is available yet.')
|
||||||
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
||||||
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
||||||
->assertSee($explanation?->nextActionText ?? '')
|
->assertSee($explanation?->nextActionText ?? '')
|
||||||
->assertSee('The run completed, but normal output was intentionally suppressed.')
|
|
||||||
->assertSee('Resume or rerun evidence capture before relying on this compare result.')
|
->assertSee('Resume or rerun evidence capture before relying on this compare result.')
|
||||||
->assertDontSee('Artifact next step');
|
->assertDontSee('Artifact next step');
|
||||||
|
|
||||||
@ -206,7 +205,7 @@ function visibleLivewireText(Testable $component): string
|
|||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||||
->assertSee($explanation?->headline ?? '')
|
->assertSee('The compare finished, but a compare strategy failure kept the result incomplete.')
|
||||||
->assertSee($explanation?->nextActionText ?? '')
|
->assertSee($explanation?->nextActionText ?? '')
|
||||||
->assertSee('Compare strategy')
|
->assertSee('Compare strategy')
|
||||||
->assertSee('Intune Policy')
|
->assertSee('Intune Policy')
|
||||||
@ -314,7 +313,7 @@ function visibleLivewireText(Testable $component): string
|
|||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||||
->assertSee($explanation?->headline ?? '')
|
->assertSee('The compare finished, but no decision-grade result is available yet.')
|
||||||
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
||||||
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
||||||
->assertDontSee('No confirmed drift in the latest baseline compare.');
|
->assertDontSee('No confirmed drift in the latest baseline compare.');
|
||||||
|
|||||||
@ -43,7 +43,7 @@
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
->test(TenantlessOperationRunViewer::class, ['run' => $run])
|
||||||
->assertSee('Artifact truth')
|
->assertSee('Artifact truth')
|
||||||
->assertSee($explanation?->headline ?? '')
|
->assertSee('The snapshot finished processing, but its evidence basis is incomplete.')
|
||||||
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
->assertSee($explanation?->evaluationResultLabel() ?? '')
|
||||||
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
->assertSee($explanation?->trustworthinessLabel() ?? '')
|
||||||
->assertSee('Partially complete')
|
->assertSee('Partially complete')
|
||||||
|
|||||||
@ -0,0 +1,177 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Operations\TenantlessOperationRunViewer;
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Livewire\Features\SupportTesting\Testable;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Tests\Feature\Concerns\BuildsGovernanceArtifactTruthFixtures;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class, BuildsGovernanceArtifactTruthFixtures::class);
|
||||||
|
|
||||||
|
function governanceVisibleText(Testable $component): string
|
||||||
|
{
|
||||||
|
$html = $component->html();
|
||||||
|
$html = preg_replace('/<script\b[^>]*>.*?<\/script>/is', '', $html) ?? $html;
|
||||||
|
$html = preg_replace('/<style\b[^>]*>.*?<\/style>/is', '', $html) ?? $html;
|
||||||
|
$html = preg_replace('/\s+wire:snapshot="[^"]*"/', '', $html) ?? $html;
|
||||||
|
$html = preg_replace('/\s+wire:effects="[^"]*"/', '', $html) ?? $html;
|
||||||
|
|
||||||
|
return trim((string) preg_replace('/\s+/', ' ', strip_tags($html)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function governanceRunViewer(TestCase $testCase, $user, Tenant $tenant, OperationRun $run): Testable
|
||||||
|
{
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
$testCase->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
||||||
|
session([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]);
|
||||||
|
|
||||||
|
return Livewire::actingAs($user)
|
||||||
|
->test(TenantlessOperationRunViewer::class, ['run' => $run]);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('renders a summary-first hierarchy for zero-output baseline compare runs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'partially_succeeded',
|
||||||
|
'context' => [
|
||||||
|
'baseline_compare' => [
|
||||||
|
'reason_code' => 'coverage_unproven',
|
||||||
|
'coverage' => [
|
||||||
|
'proof' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'summary_counts' => [
|
||||||
|
'total' => 0,
|
||||||
|
'processed' => 0,
|
||||||
|
'errors_recorded' => 1,
|
||||||
|
],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = governanceRunViewer($this, $user, $tenant, $run)
|
||||||
|
->assertSee('Decision')
|
||||||
|
->assertSee('Artifact impact')
|
||||||
|
->assertSee('Dominant cause')
|
||||||
|
->assertSee('Primary next step')
|
||||||
|
->assertSee('The compare finished, but no decision-grade result is available yet.')
|
||||||
|
->assertSee('Artifact truth details')
|
||||||
|
->assertSee('Monitoring detail');
|
||||||
|
|
||||||
|
$pageText = governanceVisibleText($component);
|
||||||
|
|
||||||
|
expect(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details'))
|
||||||
|
->and(mb_strpos($pageText, 'Decision'))->toBeLessThan(mb_strpos($pageText, 'Monitoring detail'))
|
||||||
|
->and($pageText)->toContain('no decision-grade result is available yet');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps blocked baseline capture summaries ahead of diagnostics without adding new run-detail actions', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_capture',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'blocked',
|
||||||
|
'context' => [
|
||||||
|
'reason_code' => 'missing_capability',
|
||||||
|
'baseline_capture' => [
|
||||||
|
'subjects_total' => 0,
|
||||||
|
'gaps' => [
|
||||||
|
'count' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'failure_summary' => [[
|
||||||
|
'reason_code' => 'missing_capability',
|
||||||
|
'message' => 'A required capability is missing for this run.',
|
||||||
|
]],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$component = governanceRunViewer($this, $user, $tenant, $run)
|
||||||
|
->assertActionVisible('operate_hub_back_to_operations')
|
||||||
|
->assertActionVisible('refresh')
|
||||||
|
->assertSee('Blocked by prerequisite')
|
||||||
|
->assertSee('No baseline was captured')
|
||||||
|
->assertSee('Artifact impact')
|
||||||
|
->assertSee('Dominant cause');
|
||||||
|
|
||||||
|
$pageText = governanceVisibleText($component);
|
||||||
|
|
||||||
|
expect(mb_substr_count($pageText, 'No baseline was captured'))->toBe(1)
|
||||||
|
->and(mb_strpos($pageText, 'No baseline was captured'))->toBeLessThan(mb_strpos($pageText, 'Artifact truth details'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows processing outcome separately from artifact impact for stale evidence snapshot runs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = $this->makeArtifactTruthRun($tenant, 'tenant.evidence.snapshot.generate');
|
||||||
|
|
||||||
|
$this->makeStaleArtifactTruthEvidenceSnapshot(
|
||||||
|
tenant: $tenant,
|
||||||
|
snapshotOverrides: [
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
governanceRunViewer($this, $user, $tenant, $run)
|
||||||
|
->assertSee('Outcome')
|
||||||
|
->assertSee('Artifact impact')
|
||||||
|
->assertSee('Completed successfully')
|
||||||
|
->assertSee('The snapshot finished processing, but its evidence basis is already stale.')
|
||||||
|
->assertSee('Result trust');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves a dominant cause plus secondary causes for degraded review composition runs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = $this->makeArtifactTruthRun($tenant, 'tenant.review.compose');
|
||||||
|
$snapshot = $this->makeStaleArtifactTruthEvidenceSnapshot($tenant);
|
||||||
|
|
||||||
|
$this->makeArtifactTruthReview(
|
||||||
|
tenant: $tenant,
|
||||||
|
user: $user,
|
||||||
|
snapshot: $snapshot,
|
||||||
|
reviewOverrides: [
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
'completeness_state' => 'partial',
|
||||||
|
],
|
||||||
|
summaryOverrides: [
|
||||||
|
'section_state_counts' => [
|
||||||
|
'complete' => 4,
|
||||||
|
'partial' => 1,
|
||||||
|
'missing' => 1,
|
||||||
|
'stale' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$component = governanceRunViewer($this, $user, $tenant, $run)
|
||||||
|
->assertSee('Dominant cause')
|
||||||
|
->assertSee('Missing sections')
|
||||||
|
->assertSee('Secondary causes')
|
||||||
|
->assertSee('Stale evidence basis');
|
||||||
|
|
||||||
|
$pageText = governanceVisibleText($component);
|
||||||
|
|
||||||
|
expect(mb_strpos($pageText, 'Missing sections'))->toBeLessThan(mb_strpos($pageText, 'Secondary causes'))
|
||||||
|
->and($pageText)->toContain('stale evidence');
|
||||||
|
});
|
||||||
@ -61,6 +61,32 @@
|
|||||||
expect($explanation->family)->toBe(ExplanationFamily::TrustworthyResult)
|
expect($explanation->family)->toBe(ExplanationFamily::TrustworthyResult)
|
||||||
->and($explanation->evaluationResult)->toBe('full_result')
|
->and($explanation->evaluationResult)->toBe('full_result')
|
||||||
->and($explanation->trustworthinessLevel)->toBe(TrustworthinessLevel::Trustworthy)
|
->and($explanation->trustworthinessLevel)->toBe(TrustworthinessLevel::Trustworthy)
|
||||||
|
->and($explanation->nextActionCategory)->toBe('none')
|
||||||
->and($explanation->nextActionText)->toBe('No action needed')
|
->and($explanation->nextActionText)->toBe('No action needed')
|
||||||
->and($explanation->coverageStatement)->toContain('sufficient');
|
->and($explanation->coverageStatement)->toContain('sufficient');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps retryable transient reasons into retry-later guidance', function (): void {
|
||||||
|
$reason = $this->makeExplanationReasonEnvelope([
|
||||||
|
'internalCode' => 'baseline_capture_transient_timeout',
|
||||||
|
'operatorLabel' => 'Capture paused',
|
||||||
|
'shortExplanation' => 'The capture hit a transient timeout while collecting evidence.',
|
||||||
|
'actionability' => 'retryable_transient',
|
||||||
|
'nextSteps' => [\App\Support\ReasonTranslation\NextStepOption::instruction('Retry the capture after worker capacity recovers.')],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$truth = $this->makeArtifactTruthEnvelope([
|
||||||
|
'executionOutcome' => 'partially_succeeded',
|
||||||
|
'artifactExistence' => 'created_but_not_usable',
|
||||||
|
'contentState' => 'missing_input',
|
||||||
|
'actionability' => 'required',
|
||||||
|
'primaryLabel' => 'Artifact not usable',
|
||||||
|
'primaryExplanation' => 'The capture did not finish cleanly enough to produce a usable artifact.',
|
||||||
|
'nextActionLabel' => 'Retry the capture after worker capacity recovers',
|
||||||
|
], $reason);
|
||||||
|
|
||||||
|
$explanation = app(OperatorExplanationBuilder::class)->fromArtifactTruthEnvelope($truth);
|
||||||
|
|
||||||
|
expect($explanation->nextActionCategory)->toBe('retry_later')
|
||||||
|
->and($explanation->nextActionText)->toBe('Retry the capture after worker capacity recovers');
|
||||||
|
});
|
||||||
|
|||||||
@ -0,0 +1,231 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\OperationRun;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Support\OpsUx\GovernanceRunDiagnosticSummaryBuilder;
|
||||||
|
use App\Support\OpsUx\SummaryCountsNormalizer;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\Feature\Concerns\BuildsGovernanceArtifactTruthFixtures;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class, BuildsGovernanceArtifactTruthFixtures::class);
|
||||||
|
|
||||||
|
it('derives a blocked baseline capture summary with prerequisite-focused next steps', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_capture',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'blocked',
|
||||||
|
'context' => [
|
||||||
|
'reason_code' => 'missing_capability',
|
||||||
|
'baseline_capture' => [
|
||||||
|
'subjects_total' => 0,
|
||||||
|
'gaps' => [
|
||||||
|
'count' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'failure_summary' => [[
|
||||||
|
'reason_code' => 'missing_capability',
|
||||||
|
'message' => 'A required capability is missing for this run.',
|
||||||
|
]],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run);
|
||||||
|
|
||||||
|
expect($summary)->not->toBeNull()
|
||||||
|
->and($summary?->headline)->toContain('No baseline was captured')
|
||||||
|
->and($summary?->dominantCause['label'])->toBe('No governed subjects captured')
|
||||||
|
->and($summary?->nextActionCategory)->toBe('refresh_prerequisite_data')
|
||||||
|
->and($summary?->affectedScaleCue['label'])->toBe('Capture scope')
|
||||||
|
->and($summary?->affectedScaleCue['value'])->toContain('0 governed subjects');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives an ambiguous baseline compare summary with affected scale and scope review guidance', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'partially_succeeded',
|
||||||
|
'context' => [
|
||||||
|
'baseline_compare' => [
|
||||||
|
'reason_code' => 'ambiguous_subjects',
|
||||||
|
'subjects_total' => 12,
|
||||||
|
'evidence_gaps' => [
|
||||||
|
'count' => 4,
|
||||||
|
],
|
||||||
|
'coverage' => [
|
||||||
|
'proof' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'summary_counts' => [
|
||||||
|
'total' => 0,
|
||||||
|
'processed' => 0,
|
||||||
|
'errors_recorded' => 2,
|
||||||
|
],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run);
|
||||||
|
|
||||||
|
expect($summary)->not->toBeNull()
|
||||||
|
->and($summary?->headline)->toContain('ambiguous subject matching')
|
||||||
|
->and($summary?->dominantCause['label'])->toBe('Ambiguous matches')
|
||||||
|
->and($summary?->nextActionCategory)->toBe('review_scope_or_ambiguous_matches')
|
||||||
|
->and($summary?->affectedScaleCue['label'])->toBe('Affected subjects')
|
||||||
|
->and($summary?->affectedScaleCue['value'])->toContain('4 governed subjects');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps execution outcome separate from artifact impact for stale evidence snapshot runs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = $this->makeArtifactTruthRun($tenant, 'tenant.evidence.snapshot.generate');
|
||||||
|
|
||||||
|
$this->makeStaleArtifactTruthEvidenceSnapshot(
|
||||||
|
tenant: $tenant,
|
||||||
|
snapshotOverrides: [
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run->fresh());
|
||||||
|
|
||||||
|
expect($summary)->not->toBeNull()
|
||||||
|
->and($summary?->executionOutcomeLabel)->toBe('Completed successfully')
|
||||||
|
->and($summary?->artifactImpactLabel)->not->toBe($summary?->executionOutcomeLabel)
|
||||||
|
->and($summary?->headline)->toContain('stale')
|
||||||
|
->and($summary?->nextActionCategory)->toBe('refresh_prerequisite_data');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives resume capture or generation when a compare run records a resume token', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'partially_succeeded',
|
||||||
|
'context' => [
|
||||||
|
'baseline_compare' => [
|
||||||
|
'resume_token' => 'resume-token-220',
|
||||||
|
'evidence_gaps' => [
|
||||||
|
'count' => 2,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run);
|
||||||
|
|
||||||
|
expect($summary)->not->toBeNull()
|
||||||
|
->and($summary?->nextActionCategory)->toBe('resume_capture_or_generation')
|
||||||
|
->and($summary?->headline)->toContain('evidence capture still needs to resume');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps deterministic multi-cause ordering for degraded review composition runs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = $this->makeArtifactTruthRun($tenant, 'tenant.review.compose');
|
||||||
|
$snapshot = $this->makeStaleArtifactTruthEvidenceSnapshot($tenant, [
|
||||||
|
'operation_run_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->makeArtifactTruthReview(
|
||||||
|
tenant: $tenant,
|
||||||
|
user: $user,
|
||||||
|
snapshot: $snapshot,
|
||||||
|
reviewOverrides: [
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
'completeness_state' => 'partial',
|
||||||
|
],
|
||||||
|
summaryOverrides: [
|
||||||
|
'section_state_counts' => [
|
||||||
|
'complete' => 4,
|
||||||
|
'partial' => 1,
|
||||||
|
'missing' => 1,
|
||||||
|
'stale' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$builder = app(GovernanceRunDiagnosticSummaryBuilder::class);
|
||||||
|
$first = $builder->build($run->fresh());
|
||||||
|
$second = $builder->build($run->fresh());
|
||||||
|
|
||||||
|
expect($first)->not->toBeNull()
|
||||||
|
->and($second)->not->toBeNull()
|
||||||
|
->and($first?->dominantCause['label'])->toBe('Missing sections')
|
||||||
|
->and($first?->secondaryCauses[0]['label'] ?? null)->toBe('Stale evidence basis')
|
||||||
|
->and($first?->secondaryCauses)->toEqual($second?->secondaryCauses)
|
||||||
|
->and($first?->headline)->toContain('missing sections and stale evidence');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('derives no further action for publishable review pack runs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$run = $this->makeArtifactTruthRun($tenant, 'tenant.review_pack.generate');
|
||||||
|
$snapshot = $this->makeArtifactTruthEvidenceSnapshot($tenant, [
|
||||||
|
'operation_run_id' => null,
|
||||||
|
]);
|
||||||
|
$review = $this->makeArtifactTruthReview($tenant, $user, $snapshot, [
|
||||||
|
'operation_run_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->makeArtifactTruthReviewPack(
|
||||||
|
tenant: $tenant,
|
||||||
|
user: $user,
|
||||||
|
snapshot: $snapshot,
|
||||||
|
review: $review,
|
||||||
|
packOverrides: [
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run->fresh());
|
||||||
|
|
||||||
|
expect($summary)->not->toBeNull()
|
||||||
|
->and($summary?->nextActionCategory)->toBe('no_further_action')
|
||||||
|
->and($summary?->nextActionText)->toBe('No action needed.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not invent new summary count keys while deriving scale cues', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
|
||||||
|
$run = OperationRun::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'type' => 'baseline_compare',
|
||||||
|
'status' => 'completed',
|
||||||
|
'outcome' => 'partially_succeeded',
|
||||||
|
'summary_counts' => [
|
||||||
|
'total' => 7,
|
||||||
|
'custom_noise' => 99,
|
||||||
|
],
|
||||||
|
'context' => [
|
||||||
|
'baseline_compare' => [],
|
||||||
|
],
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$summary = app(GovernanceRunDiagnosticSummaryBuilder::class)->build($run);
|
||||||
|
|
||||||
|
expect($summary)->not->toBeNull()
|
||||||
|
->and(array_keys(SummaryCountsNormalizer::normalize(is_array($run->summary_counts) ? $run->summary_counts : [])))
|
||||||
|
->toBe(['total'])
|
||||||
|
->and($summary?->affectedScaleCue['source'])->toBe('summary_counts')
|
||||||
|
->and($summary?->affectedScaleCue['label'])->toBe('Total');
|
||||||
|
});
|
||||||
@ -26,7 +26,7 @@ ### Governance & Architecture Hardening
|
|||||||
|
|
||||||
**Active specs**: 144
|
**Active specs**: 144
|
||||||
**Specced follow-through (draft)**: 149 (queued execution reauthorization), 150 (tenant-owned query canon), 151 (findings workflow backstop), 152 (Livewire context locking), 214 (governance outcome compression), 216 (provider dispatch gate)
|
**Specced follow-through (draft)**: 149 (queued execution reauthorization), 150 (tenant-owned query canon), 151 (findings workflow backstop), 152 (Livewire context locking), 214 (governance outcome compression), 216 (provider dispatch gate)
|
||||||
**Operator truth initiative** (sequenced): Operator Outcome Taxonomy (Spec 156) → Reason Code Translation (Spec 157) → Artifact Truth Semantics (Spec 158) → Governance Operator Outcome Compression (Spec 214, draft). Humanized Diagnostic Summaries for Governance Operations remains the next open adoption slice, while Provider Dispatch Gate Unification is now Spec 216 (draft) as the adjacent hardening lane.
|
**Operator truth initiative** (sequenced): Operator Outcome Taxonomy (Spec 156) → Reason Code Translation (Spec 157) → Artifact Truth Semantics (Spec 158) → Governance Operator Outcome Compression (Spec 214, draft). Humanized Diagnostic Summaries for Governance Operations is now Spec 220 (draft) as the run-detail adoption slice, while Provider Dispatch Gate Unification is now Spec 216 (draft) as the adjacent hardening lane.
|
||||||
**Source**: architecture audit 2026-03-15, audit constitution, semantic clarity audit 2026-03-21, product spec-candidates
|
**Source**: architecture audit 2026-03-15, audit constitution, semantic clarity audit 2026-03-21, product spec-candidates
|
||||||
|
|
||||||
### UI & Product Maturity Polish
|
### UI & Product Maturity Polish
|
||||||
|
|||||||
@ -42,6 +42,7 @@ ## Promoted to Spec
|
|||||||
- Request-Scoped Derived State and Resolver Memoization → Spec 167 (`derived-state-memoization`)
|
- Request-Scoped Derived State and Resolver Memoization → Spec 167 (`derived-state-memoization`)
|
||||||
- Tenant Governance Aggregate Contract → Spec 168 (`tenant-governance-aggregate-contract`)
|
- Tenant Governance Aggregate Contract → Spec 168 (`tenant-governance-aggregate-contract`)
|
||||||
- Governance Operator Outcome Compression → Spec 214 (`governance-outcome-compression`)
|
- Governance Operator Outcome Compression → Spec 214 (`governance-outcome-compression`)
|
||||||
|
- Humanized Diagnostic Summaries for Governance Operations → Spec 220 (`governance-run-summaries`)
|
||||||
- Provider-Backed Action Preflight and Dispatch Gate Unification → Spec 216 (`provider-dispatch-gate`)
|
- Provider-Backed Action Preflight and Dispatch Gate Unification → Spec 216 (`provider-dispatch-gate`)
|
||||||
- Record Page Header Discipline & Contextual Navigation → Spec 192 (`record-header-discipline`)
|
- Record Page Header Discipline & Contextual Navigation → Spec 192 (`record-header-discipline`)
|
||||||
- Monitoring Surface Action Hierarchy & Workbench Semantics → Spec 193 (`monitoring-action-hierarchy`)
|
- Monitoring Surface Action Hierarchy & Workbench Semantics → Spec 193 (`monitoring-action-hierarchy`)
|
||||||
@ -144,37 +145,6 @@ ### Baseline Capture Truthful Outcomes and Upstream Guardrails
|
|||||||
- **Dependencies**: Baseline drift engine stable (Specs 116–119), inventory coverage context, canonical operation-run presentation work (Specs 054, 114, 144), audit log foundation (Spec 134), tenant operability and execution legitimacy direction (Specs 148, 149)
|
- **Dependencies**: Baseline drift engine stable (Specs 116–119), inventory coverage context, canonical operation-run presentation work (Specs 054, 114, 144), audit log foundation (Spec 134), tenant operability and execution legitimacy direction (Specs 148, 149)
|
||||||
- **Related specs / candidates**: Spec 101 (baseline governance), Specs 116–119 (baseline drift engine), Spec 144 (canonical operation viewer context decoupling), Spec 149 (queued execution reauthorization), `discoveries.md` entry "Drift engine hard-fail when no Inventory Sync exists"
|
- **Related specs / candidates**: Spec 101 (baseline governance), Specs 116–119 (baseline drift engine), Spec 144 (canonical operation viewer context decoupling), Spec 149 (queued execution reauthorization), `discoveries.md` entry "Drift engine hard-fail when no Inventory Sync exists"
|
||||||
|
|
||||||
### Humanized Diagnostic Summaries for Governance Operations
|
|
||||||
- **Type**: hardening
|
|
||||||
- **Source**: operator-facing governance run-detail gap analysis 2026-03-23; follow-up to Specs 156, 157, and 158
|
|
||||||
- **Vehicle**: new standalone candidate
|
|
||||||
- **Problem**: Governance run-detail pages now have the right outcome, reason, and artifact-truth semantics, but the operator explanation often still lives in raw JSON. A run can read as `Completed with follow-up`, `Partial`, `Blocked`, or `Missing input` while the important meaning stays hidden: how much was affected, which cause dominates, whether retry or resume helps, and whether the resulting artifact is actually trustworthy.
|
|
||||||
- **Why it matters**: This is the missing middle layer between Spec 158's truth engine and the operator's actual decision. Without it, TenantPilot stays semantically correct but too technical on one of its highest-trust governance surfaces. Raw JSON remains part of normal troubleshooting when it should be optional.
|
|
||||||
- **Proposed direction**:
|
|
||||||
- Add a humanized diagnostic summary layer on governance run-detail pages between semantic verdicts and raw JSON
|
|
||||||
- Lead with impact, dominant cause, artifact trustworthiness, and next action instead of forcing operators to infer those from badges plus raw context
|
|
||||||
- Render a compact dominant-cause breakdown for multi-cause degraded runs, including counts or relative scale where useful
|
|
||||||
- Separate processing-success counters from artifact usability so technically correct metrics do not read as false-green artifact success
|
|
||||||
- Upgrade generic `Inspect diagnostics` guidance into cause-aware next steps such as retry later, resume capture, refresh policy inventory, verify missing policies, review ambiguous matches, or fix access or scope configuration
|
|
||||||
- Keep raw JSON and low-level context fully available, but explicitly secondary
|
|
||||||
- **Primary adoption surfaces**:
|
|
||||||
- Canonical Monitoring run-detail pages for governance operation types
|
|
||||||
- Shared tenantless canonical run viewers and run-detail templates
|
|
||||||
- Governance run detail reached from baseline capture, baseline compare, evidence refresh or snapshot generation, tenant review generation, and review-pack generation
|
|
||||||
- **Scope boundaries**:
|
|
||||||
- **In scope**: run-detail explanation hierarchy, humanized impact summaries, dominant-cause breakdowns, clearer processing-versus-artifact terminology, reusable guidance pattern for governance run families
|
|
||||||
- **Out of scope**: full Operations redesign, broad list or dashboard overhaul, new persistence models for summaries, removal of raw JSON, new truth axes beyond the existing outcome or artifact-truth model, generalized rewrite of all governance artifact detail pages
|
|
||||||
- **Acceptance points**:
|
|
||||||
- A normal operator can understand the dominant problem and next step on a governance run-detail page without opening raw JSON
|
|
||||||
- Runs with technically successful processing but degraded artifacts explicitly explain why those truths diverge
|
|
||||||
- Multi-cause degraded runs show the dominant causes and their scale instead of only one flattened abstract state
|
|
||||||
- Guidance distinguishes retryable or resumable situations from structural prerequisite problems when the data supports that distinction
|
|
||||||
- Raw JSON remains present for audit and debugging but is clearly secondary in the page hierarchy
|
|
||||||
- **Dependencies**: Spec 156 (operator-outcome-taxonomy), Spec 157 (reason-code-translation), Spec 158 (artifact-truth-semantics), canonical operation viewer work, shared governance detail templates
|
|
||||||
- **Related specs / candidates**: Spec 144 (canonical operation viewer context decoupling), Spec 153 (evidence-domain-foundation), Spec 155 (tenant-review-layer), Spec 158 (artifact-truth-semantics), Spec 214 (Governance Operator Outcome Compression)
|
|
||||||
- **Strategic sequencing**: Best treated as the run-detail explainability companion to Spec 214 (Governance Operator Outcome Compression). Spec 214 improves governance artifact scan surfaces; this candidate makes governance run detail self-explanatory once an operator drills in.
|
|
||||||
- **Priority**: high
|
|
||||||
|
|
||||||
> **Operator Truth Initiative — Sequencing Note**
|
> **Operator Truth Initiative — Sequencing Note**
|
||||||
>
|
>
|
||||||
> The operator-truth work now has two connected lanes: a shared truth-foundation lane and a governance-surface compression lane. Together they address the systemic gap between backend truth richness and operator-facing truth quality without forcing operators to parse raw internal semantics.
|
> The operator-truth work now has two connected lanes: a shared truth-foundation lane and a governance-surface compression lane. Together they address the systemic gap between backend truth richness and operator-facing truth quality without forcing operators to parse raw internal semantics.
|
||||||
@ -185,7 +155,7 @@ ### Humanized Diagnostic Summaries for Governance Operations
|
|||||||
> 3. **Governance Artifact Truthful Outcomes & Fidelity Semantics** — establishes the full internal truth model for governance artifacts, keeping existence, usability, freshness, completeness, publication readiness, and actionability distinct.
|
> 3. **Governance Artifact Truthful Outcomes & Fidelity Semantics** — establishes the full internal truth model for governance artifacts, keeping existence, usability, freshness, completeness, publication readiness, and actionability distinct.
|
||||||
> 4. **Operator Explanation Layer for Degraded / Partial / Suppressed Results** — defines the cross-cutting interpretation layer that turns internal truth dimensions into operator-readable explanation: multi-dimensional outcome separation (execution, evaluation, reliability, coverage, action), shared explanation patterns for degraded/partial/suppressed states, count semantics rules, "why no findings" patterns, and reliability visibility. Consumes the taxonomy, translation, and artifact-truth foundations; provides the shared explanation pattern library that compression and humanized summaries adopt on their respective surfaces. Baseline Compare is the golden-path reference implementation.
|
> 4. **Operator Explanation Layer for Degraded / Partial / Suppressed Results** — defines the cross-cutting interpretation layer that turns internal truth dimensions into operator-readable explanation: multi-dimensional outcome separation (execution, evaluation, reliability, coverage, action), shared explanation patterns for degraded/partial/suppressed states, count semantics rules, "why no findings" patterns, and reliability visibility. Consumes the taxonomy, translation, and artifact-truth foundations; provides the shared explanation pattern library that compression and humanized summaries adopt on their respective surfaces. Baseline Compare is the golden-path reference implementation.
|
||||||
> 5. **Governance Operator Outcome Compression** — now promoted to Spec 214, applying the foundation and explanation layer to governance workflow surfaces so lists and details answer the operator's primary question first while preserving diagnostics as second-layer detail.
|
> 5. **Governance Operator Outcome Compression** — now promoted to Spec 214, applying the foundation and explanation layer to governance workflow surfaces so lists and details answer the operator's primary question first while preserving diagnostics as second-layer detail.
|
||||||
> 6. **Humanized Diagnostic Summaries for Governance Operations** — the run-detail explainability companion to compression; makes governance run detail self-explanatory using the explanation patterns established in step 4.
|
> 6. **Humanized Diagnostic Summaries for Governance Operations** — now promoted to Spec 220 (`governance-run-summaries`), the run-detail explainability companion to compression that makes governance run detail self-explanatory using the explanation patterns established in step 4.
|
||||||
> 7. **Provider-Backed Action Preflight and Dispatch Gate Unification** — now promoted to Spec 216, extending the proven Gen 2 gate pattern to all provider-backed operations and establishing a shared result presenter as the adjacent hardening lane.
|
> 7. **Provider-Backed Action Preflight and Dispatch Gate Unification** — now promoted to Spec 216, extending the proven Gen 2 gate pattern to all provider-backed operations and establishing a shared result presenter as the adjacent hardening lane.
|
||||||
>
|
>
|
||||||
> **Why this order rather than the inverse:** The semantic-clarity audit classified the taxonomy problem as P0 (60% of warning badges are false alarms — actively damages operator trust). Reason code translation creates the shared human-facing language. Spec 158 establishes the correct internal truth engine for governance artifacts. The Operator Explanation Layer then defines the cross-cutting interpretation patterns that all downstream surfaces need — the systemwide rules for how degraded, partial, suppressed, and incomplete results are separated, explained, and acted upon. Compression and humanized summaries are adoption slices that apply those patterns to specific surface families (governance artifact lists and governance run details respectively). Gate unification remains highly valuable but is a neighboring hardening lane.
|
> **Why this order rather than the inverse:** The semantic-clarity audit classified the taxonomy problem as P0 (60% of warning badges are false alarms — actively damages operator trust). Reason code translation creates the shared human-facing language. Spec 158 establishes the correct internal truth engine for governance artifacts. The Operator Explanation Layer then defines the cross-cutting interpretation patterns that all downstream surfaces need — the systemwide rules for how degraded, partial, suppressed, and incomplete results are separated, explained, and acted upon. Compression and humanized summaries are adoption slices that apply those patterns to specific surface families (governance artifact lists and governance run details respectively). Gate unification remains highly valuable but is a neighboring hardening lane.
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
# Specification Quality Checklist: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||||
|
**Created**: 2026-04-20
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] No implementation details (languages, frameworks, APIs)
|
||||||
|
- [x] Focused on user value and business needs
|
||||||
|
- [x] Written for non-technical stakeholders
|
||||||
|
- [x] All mandatory sections completed
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||||
|
- [x] Requirements are testable and unambiguous
|
||||||
|
- [x] Success criteria are measurable
|
||||||
|
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||||
|
- [x] All acceptance scenarios are defined
|
||||||
|
- [x] Edge cases are identified
|
||||||
|
- [x] Scope is clearly bounded
|
||||||
|
- [x] Dependencies and assumptions identified
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] All functional requirements have clear acceptance criteria
|
||||||
|
- [x] User scenarios cover primary flows
|
||||||
|
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||||
|
- [x] No implementation details leak into specification
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Validation pass 1 complete.
|
||||||
|
- Required surface-governance metadata such as routes and action-matrix references are present, but the spec avoids implementation mechanics, framework instructions, and code-level solution design.
|
||||||
@ -0,0 +1,230 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: Governance Operation Run Summaries Contract
|
||||||
|
version: 1.0.0
|
||||||
|
description: >-
|
||||||
|
Internal reference contract for Spec 220. These routes continue to return
|
||||||
|
HTML through Filament and Livewire. The vendor media types below document
|
||||||
|
the logical summary payloads that must be derivable before rendering. This
|
||||||
|
is not a public API commitment.
|
||||||
|
paths:
|
||||||
|
/admin/operations:
|
||||||
|
get:
|
||||||
|
summary: Canonical operations list entry point
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Rendered canonical operations list page
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
application/vnd.tenantpilot.governance-operations-list+json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GovernanceOperationsListPage'
|
||||||
|
'404':
|
||||||
|
description: Workspace context is missing or the viewer is not entitled to the canonical monitoring scope
|
||||||
|
/admin/operations/{run}:
|
||||||
|
get:
|
||||||
|
summary: Canonical governance operation run detail
|
||||||
|
parameters:
|
||||||
|
- name: run
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Rendered canonical governance run-detail page
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
application/vnd.tenantpilot.governance-operation-run-detail+json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/GovernanceOperationRunDetailPage'
|
||||||
|
'403':
|
||||||
|
description: Viewer is in scope but lacks required capability for a related action
|
||||||
|
'404':
|
||||||
|
description: Run is not visible because it does not exist or entitlement is missing
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
GovernanceOperationsListPage:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- activeContext
|
||||||
|
- rowInspectModel
|
||||||
|
properties:
|
||||||
|
activeContext:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
workspaceScope:
|
||||||
|
type: string
|
||||||
|
tenantContextActive:
|
||||||
|
type: boolean
|
||||||
|
rowInspectModel:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- row_click
|
||||||
|
canonicalDetailRoute:
|
||||||
|
type: string
|
||||||
|
example: /admin/operations/44
|
||||||
|
GovernanceOperationRunDetailPage:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- runId
|
||||||
|
- canonicalOperationType
|
||||||
|
- summary
|
||||||
|
- diagnosticsAvailable
|
||||||
|
properties:
|
||||||
|
runId:
|
||||||
|
type: integer
|
||||||
|
canonicalOperationType:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- baseline.capture
|
||||||
|
- baseline.compare
|
||||||
|
- tenant.evidence.snapshot.generate
|
||||||
|
- tenant.review.compose
|
||||||
|
- tenant.review_pack.generate
|
||||||
|
artifactFamily:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
enum:
|
||||||
|
- baseline_snapshot
|
||||||
|
- evidence_snapshot
|
||||||
|
- tenant_review
|
||||||
|
- review_pack
|
||||||
|
- null
|
||||||
|
summary:
|
||||||
|
$ref: '#/components/schemas/GovernanceRunDiagnosticSummary'
|
||||||
|
relatedNavigation:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/RelatedNavigationLink'
|
||||||
|
diagnosticsAvailable:
|
||||||
|
type: boolean
|
||||||
|
diagnosticsSections:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/DiagnosticsSection'
|
||||||
|
GovernanceRunDiagnosticSummary:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- headline
|
||||||
|
- executionOutcomeLabel
|
||||||
|
- artifactImpactLabel
|
||||||
|
- primaryReason
|
||||||
|
- nextActionText
|
||||||
|
properties:
|
||||||
|
headline:
|
||||||
|
type: string
|
||||||
|
executionOutcomeLabel:
|
||||||
|
type: string
|
||||||
|
artifactImpactLabel:
|
||||||
|
type: string
|
||||||
|
primaryReason:
|
||||||
|
type: string
|
||||||
|
affectedScaleCue:
|
||||||
|
$ref: '#/components/schemas/AffectedScaleCue'
|
||||||
|
nextActionText:
|
||||||
|
type: string
|
||||||
|
dominantCause:
|
||||||
|
$ref: '#/components/schemas/DominantCauseBreakdown'
|
||||||
|
secondaryFacts:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/SummaryFact'
|
||||||
|
DominantCauseBreakdown:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- primaryLabel
|
||||||
|
- primaryExplanation
|
||||||
|
properties:
|
||||||
|
primaryCode:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
primaryLabel:
|
||||||
|
type: string
|
||||||
|
primaryExplanation:
|
||||||
|
type: string
|
||||||
|
secondaryCauses:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
AffectedScaleCue:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- label
|
||||||
|
- value
|
||||||
|
- source
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
source:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- summary_counts
|
||||||
|
- context
|
||||||
|
- related_artifact_truth
|
||||||
|
confidence:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- exact
|
||||||
|
- bounded
|
||||||
|
- best_available
|
||||||
|
SummaryFact:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- label
|
||||||
|
- value
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
value:
|
||||||
|
type: string
|
||||||
|
emphasis:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- neutral
|
||||||
|
- caution
|
||||||
|
- blocked
|
||||||
|
RelatedNavigationLink:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- label
|
||||||
|
- visible
|
||||||
|
properties:
|
||||||
|
label:
|
||||||
|
type: string
|
||||||
|
href:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
visible:
|
||||||
|
type: boolean
|
||||||
|
deniedReason:
|
||||||
|
type:
|
||||||
|
- string
|
||||||
|
- 'null'
|
||||||
|
DiagnosticsSection:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- kind
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
kind:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- supporting_detail
|
||||||
|
- count_diagnostics
|
||||||
|
- failure_payload
|
||||||
|
- evidence_gap_detail
|
||||||
|
- type_specific_detail
|
||||||
|
collapsedByDefault:
|
||||||
|
type: boolean
|
||||||
197
specs/220-governance-run-summaries/data-model.md
Normal file
197
specs/220-governance-run-summaries/data-model.md
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
# Data Model: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This feature does not add or modify persisted domain entities. It adds a logical derived presentation model for canonical governance operation run detail under `/admin/operations/{run}`.
|
||||||
|
|
||||||
|
The design constraint is strict:
|
||||||
|
|
||||||
|
- `OperationRun` remains the only persisted source for run lifecycle and execution truth.
|
||||||
|
- Related artifacts such as `BaselineSnapshot`, `EvidenceSnapshot`, `TenantReview`, and `ReviewPack` remain the persisted source for artifact truth where they exist.
|
||||||
|
- `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, and `ReasonPresenter` remain the semantic inputs.
|
||||||
|
- The new summary remains fully derived and surface-specific.
|
||||||
|
|
||||||
|
## Existing Persistent Inputs
|
||||||
|
|
||||||
|
### 1. OperationRun
|
||||||
|
|
||||||
|
- Purpose: Canonical operational record for background and governance work.
|
||||||
|
- Key persisted fields used by this feature:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `type`
|
||||||
|
- `status`
|
||||||
|
- `outcome`
|
||||||
|
- `context`
|
||||||
|
- `summary_counts`
|
||||||
|
- `failure_summary`
|
||||||
|
- `started_at`
|
||||||
|
- `completed_at`
|
||||||
|
- Relationships and derived lookups used by this feature:
|
||||||
|
- workspace and tenant context
|
||||||
|
- related artifact resolution through current operation catalog and presenter logic
|
||||||
|
|
||||||
|
### 2. Related Governance Artifacts
|
||||||
|
|
||||||
|
These are not newly modeled by this feature, but they remain relevant when a run produced or references an artifact.
|
||||||
|
|
||||||
|
- `BaselineSnapshot`
|
||||||
|
- `EvidenceSnapshot`
|
||||||
|
- `TenantReview`
|
||||||
|
- `ReviewPack`
|
||||||
|
|
||||||
|
The feature only reads their already-derived truth where available.
|
||||||
|
|
||||||
|
## Existing Derived Inputs
|
||||||
|
|
||||||
|
### A. ArtifactTruthEnvelope
|
||||||
|
|
||||||
|
`ArtifactTruthPresenter` already derives `ArtifactTruthEnvelope` for `OperationRun` and related artifact records.
|
||||||
|
|
||||||
|
Important envelope dimensions already available:
|
||||||
|
|
||||||
|
- `artifactExistence`
|
||||||
|
- `contentState`
|
||||||
|
- `freshnessState`
|
||||||
|
- `publicationReadiness`
|
||||||
|
- `supportState`
|
||||||
|
- `actionability`
|
||||||
|
- `primaryLabel`
|
||||||
|
- `primaryExplanation`
|
||||||
|
- `reason`
|
||||||
|
- `diagnosticLabel`
|
||||||
|
|
||||||
|
This feature must consume that envelope instead of replacing it.
|
||||||
|
|
||||||
|
### B. OperatorExplanationPattern
|
||||||
|
|
||||||
|
`OperatorExplanationBuilder` already derives an explanation pattern containing:
|
||||||
|
|
||||||
|
- `headline`
|
||||||
|
- `evaluationResult`
|
||||||
|
- `executionOutcome`
|
||||||
|
- `trustworthinessLevel`
|
||||||
|
- `reliabilityStatement`
|
||||||
|
- `coverageStatement`
|
||||||
|
- `dominantCauseCode`
|
||||||
|
- `dominantCauseLabel`
|
||||||
|
- `dominantCauseExplanation`
|
||||||
|
- `nextActionCategory`
|
||||||
|
- `nextActionText`
|
||||||
|
- `countDescriptors`
|
||||||
|
|
||||||
|
This feature reuses that pattern as input to the new run-detail summary.
|
||||||
|
|
||||||
|
## Derived Presentation Entities
|
||||||
|
|
||||||
|
### 1. GovernanceRunDiagnosticSummary
|
||||||
|
|
||||||
|
Primary derived object for canonical run detail.
|
||||||
|
|
||||||
|
| Field | Meaning | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| `headline` | One dominant first-pass statement for the run detail page | derived from `ArtifactTruthEnvelope` + `OperatorExplanationPattern` |
|
||||||
|
| `executionOutcomeLabel` | Technical execution result kept visible as a separate fact | `OperationRun.outcome` via existing badge semantics |
|
||||||
|
| `artifactImpactLabel` | What the resulting artifact means for operator action | artifact truth + explanation pattern |
|
||||||
|
| `primaryReason` | One short reason supporting the headline | dominant cause explanation or primary explanation |
|
||||||
|
| `affectedScaleCue` | One operator-readable scale cue, such as ambiguous subjects or missing sections | `summary_counts`, run `context`, or related artifact truth |
|
||||||
|
| `nextActionText` | First follow-up step the operator should see | existing explanation or next-step logic |
|
||||||
|
| `secondaryCauses[]` | Additional contributing causes preserved below the primary cause | ranked from reason/context inputs |
|
||||||
|
| `diagnosticsAvailable` | Whether deeper technical sections still exist below | derived from reason, payload, or technical sections |
|
||||||
|
|
||||||
|
Validation rules:
|
||||||
|
|
||||||
|
- Exactly one `headline` is allowed for the default-visible summary.
|
||||||
|
- `artifactImpactLabel` must stay distinct from `executionOutcomeLabel`.
|
||||||
|
- `affectedScaleCue` is optional, but when present it must be backed by numeric or enumerated persisted evidence, not freeform guesswork.
|
||||||
|
- `secondaryCauses[]` must not repeat the dominant cause.
|
||||||
|
|
||||||
|
### 2. DominantCauseBreakdown
|
||||||
|
|
||||||
|
Logical grouping of the main and supporting causes for degraded runs.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `primaryCauseCode` | Stable internal reason or derived cause key |
|
||||||
|
| `primaryCauseLabel` | Operator-facing dominant cause label |
|
||||||
|
| `primaryCauseExplanation` | Short explanation shown in the summary area |
|
||||||
|
| `secondaryCauses[]` | Additional causes shown in supporting detail only |
|
||||||
|
| `rankingRule` | Stable ranking rule used to keep ordering deterministic |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- Ranking must be deterministic for equivalent runs.
|
||||||
|
- The same cause class must keep the same reading direction across covered governance families.
|
||||||
|
- A run with no meaningful secondary cause data may omit the secondary list entirely.
|
||||||
|
|
||||||
|
### 3. AffectedScaleCue
|
||||||
|
|
||||||
|
Small derived object explaining what was affected and at what scale.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `label` | Operator-facing scale label such as `Affected subjects`, `Missing sections`, or `Incomplete dimensions` |
|
||||||
|
| `value` | Human-readable count or scale statement |
|
||||||
|
| `source` | Where the cue came from: `summary_counts`, `context`, or related artifact truth |
|
||||||
|
| `confidence` | Whether the cue is exact, bounded, or best available from persisted context |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- This object remains optional because not every run family has equally rich scale data.
|
||||||
|
- It must never introduce a new persisted count contract.
|
||||||
|
- It must not imply precision the persisted data does not support.
|
||||||
|
|
||||||
|
### 4. GovernanceRunSummaryContext
|
||||||
|
|
||||||
|
Logical context for the summary builder.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| `surface` | Always `canonical_operation_run_detail` for this spec |
|
||||||
|
| `canonicalOperationType` | Canonical operation type from `OperationCatalog` |
|
||||||
|
| `artifactFamily` | Related artifact family when one exists |
|
||||||
|
| `tenantVisibility` | Whether related tenant/artifact context is visible to the current actor |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- This context is surface-specific and must not become a cross-product taxonomy.
|
||||||
|
- Tenant visibility rules must suppress inaccessible related labels and links.
|
||||||
|
|
||||||
|
## Covered Run Families
|
||||||
|
|
||||||
|
| Canonical Type | Primary Artifact Family | Typical Affected-Scale Source | Dominant-Cause Focus |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `baseline.capture` | `baseline_snapshot` | `summary_counts`, `context.result`, baseline snapshot summary | blocked prerequisite, zero in-scope subjects, unusable snapshot result |
|
||||||
|
| `baseline.compare` | none direct, but linked baseline/evidence truth may exist | `summary_counts`, `context.baseline_compare`, evidence-gap payloads | suppressed output, ambiguous matches, evidence gaps, strategy failure |
|
||||||
|
| `tenant.evidence.snapshot.generate` | `evidence_snapshot` | evidence snapshot summary, completeness state, run counts | stale or incomplete evidence basis, blocked snapshot generation |
|
||||||
|
| `tenant.review.compose` | `tenant_review` | review summary, missing sections, related evidence truth | missing sections, stale evidence, internal-only review outcome |
|
||||||
|
| `tenant.review_pack.generate` | `review_pack` | pack summary, linked review state, generation context | internal-only or blocked pack outcome, source-review limitations |
|
||||||
|
|
||||||
|
## Derivation Rules
|
||||||
|
|
||||||
|
### Summary selection order
|
||||||
|
|
||||||
|
1. Resolve canonical operation type.
|
||||||
|
2. Resolve related artifact truth if present.
|
||||||
|
3. Resolve operator explanation pattern.
|
||||||
|
4. Derive dominant cause and supporting causes.
|
||||||
|
5. Derive affected-scale cue from existing persisted data.
|
||||||
|
6. Build one `GovernanceRunDiagnosticSummary`.
|
||||||
|
7. Render diagnostics below that summary without altering the underlying truth.
|
||||||
|
|
||||||
|
### Zero-output runs
|
||||||
|
|
||||||
|
- If a run completed technically but produced no decision-grade artifact, the summary must explicitly say so.
|
||||||
|
- Zero output must never default to a neutral or green reading.
|
||||||
|
|
||||||
|
### Multi-cause degraded runs
|
||||||
|
|
||||||
|
- One primary cause is required.
|
||||||
|
- Additional causes remain visible as supporting detail only.
|
||||||
|
- The ranking rule must be deterministic and shared across all covered run families.
|
||||||
|
|
||||||
|
### Authorization-sensitive output
|
||||||
|
|
||||||
|
- Related artifact names, tenant names, and links may only appear when entitlement checks already pass.
|
||||||
|
- The summary may remain useful without those labels by using generic operator-safe phrasing.
|
||||||
299
specs/220-governance-run-summaries/plan.md
Normal file
299
specs/220-governance-run-summaries/plan.md
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
# Implementation Plan: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
**Branch**: `220-governance-run-summaries` | **Date**: 2026-04-20 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/220-governance-run-summaries/spec.md`
|
||||||
|
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/220-governance-run-summaries/spec.md`
|
||||||
|
|
||||||
|
**Note**: This plan keeps the work inside the existing canonical Monitoring run-detail, artifact-truth, and operator-explanation seams. The intended implementation is a bounded derived summary layer for governance operation runs, not a new persistence model, not a new lifecycle/state family, and not a new action or surface framework.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add one operator-first diagnostic summary to canonical governance run detail so baseline capture, baseline compare, evidence snapshot generation, tenant review composition, and review-pack generation runs explain dominant artifact impact, dominant cause, affected scale, artifact trustworthiness, and next action before raw diagnostics. The implementation will reuse `TenantlessOperationRunViewer`, `OperationRunResource`, `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, `ReasonPresenter`, `OperationUxPresenter`, `SummaryCountsNormalizer`, and the current enterprise-detail builders, and it will introduce one small `GovernanceRunDiagnosticSummary` value object plus builder under `App\Support\OpsUx` so the canonical detail page can express affected-scale and multi-cause ranking without inventing a broader UI framework.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4, Blade
|
||||||
|
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, Laravel Sail, `TenantlessOperationRunViewer`, `OperationRunResource`, `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, `ReasonPresenter`, `OperationUxPresenter`, `SummaryCountsNormalizer`, and the existing enterprise-detail builders
|
||||||
|
**Storage**: PostgreSQL via existing `operation_runs` plus related `baseline_snapshots`, `evidence_snapshots`, `tenant_reviews`, and `review_packs`; no schema changes planned
|
||||||
|
**Testing**: Pest v4 unit and feature tests, focused Monitoring/Filament/Authorization coverage
|
||||||
|
**Validation Lanes**: fast-feedback, confidence
|
||||||
|
**Target Platform**: Dockerized Laravel web application via Sail locally and Linux containers in deployment
|
||||||
|
**Project Type**: Laravel monolith web application inside the `wt-plattform` monorepo
|
||||||
|
**Performance Goals**: Preserve DB-only render behavior on canonical run detail, add no render-time external calls, avoid new query breadth, and keep first-pass operator comprehension inside a 10-15 second scan window
|
||||||
|
**Constraints**: No new Graph calls, no new routes, no new `OperationRun` statuses or outcomes, no new `summary_counts` keys, no new notification surfaces, no new destructive actions, no cross-tenant leakage, and no duplication between decision summary and existing banners
|
||||||
|
**Scale/Scope**: One canonical Monitoring detail surface, five governance run families, one bounded derived summary seam, and focused regression coverage for summary ordering, multi-cause explanation, zero-output runs, and authorization safety
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: changed surfaces
|
||||||
|
- **Native vs custom classification summary**: native Filament + existing Monitoring detail primitives
|
||||||
|
- **Shared-family relevance**: governance run-detail family, operator explanation family, enterprise detail family
|
||||||
|
- **State layers in scope**: page, detail, URL-query
|
||||||
|
- **Handling modes by drift class or surface**: review-mandatory
|
||||||
|
- **Repository-signal treatment**: review-mandatory
|
||||||
|
- **Special surface test profiles**: monitoring-state-page
|
||||||
|
- **Required tests or manual smoke**: functional-core, state-contract, manual-smoke
|
||||||
|
- **Exception path and spread control**: retain the existing diagnostic-detail exception on canonical run detail; do not spread it into new surfaces or action patterns
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Passed before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
| Principle | Pre-Research | Post-Design | Notes |
|
||||||
|
|-----------|--------------|-------------|-------|
|
||||||
|
| Inventory-first / snapshots-second | PASS | PASS | The feature reorders explanation on existing run and artifact truth only; inventory and snapshot ownership remain unchanged |
|
||||||
|
| Read/write separation | PASS | PASS | No new writes, previews, confirmations, or audit-log paths are introduced |
|
||||||
|
| Graph contract path | PASS | PASS | No new Graph calls or contract-registry changes |
|
||||||
|
| RBAC / workspace / tenant isolation | PASS | PASS | Canonical `/admin/operations/{run}` remains tenant-safe; non-members stay `404`; in-scope capability denials remain `403` |
|
||||||
|
| Run observability / Ops-UX | PASS | PASS | Existing `OperationRun` lifecycle, feedback surfaces, initiator rules, and summary-count contracts remain unchanged |
|
||||||
|
| Ops-UX summary counts | PASS | PASS | Existing flat numeric `summary_counts` stay canonical; the new summary only interprets them |
|
||||||
|
| Proportionality / no premature abstraction | PASS | PASS | One bounded run-summary helper is justified; no new framework, persistence, or state family is needed |
|
||||||
|
| Few layers / UI semantics | PASS | PASS | New logic stays downstream of `ArtifactTruthEnvelope` and `OperatorExplanationPattern`; no second truth source is introduced |
|
||||||
|
| Badge semantics (BADGE-001) | PASS | PASS | Existing badge domains remain canonical; the feature changes order and supporting copy only |
|
||||||
|
| Filament-native UI (UI-FIL-001) | PASS | PASS | Existing Filament detail page, sections, and enterprise-detail builders remain the implementation path |
|
||||||
|
| Action surface / inspect model | PASS | PASS | Canonical run detail remains the single inspect model; no new row, header, or bulk actions are introduced |
|
||||||
|
| Decision-first / OPSURF | PASS | PASS | The page remains a Tertiary Evidence / Diagnostics Surface, but its first read becomes operator-first |
|
||||||
|
| Test governance (TEST-GOV-001) | PASS | PASS | Proof stays in focused Monitoring feature coverage plus one narrow unit seam; no browser or heavy-governance expansion |
|
||||||
|
| Filament v5 / Livewire v4 compliance | PASS | PASS | The work stays entirely within the current Filament v5 + Livewire v4 stack |
|
||||||
|
| Provider registration / global search / assets | PASS | PASS | No panel/provider changes, `OperationRunResource` stays non-searchable, and no new assets are required |
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: `Feature` for canonical Monitoring run detail and authorization behavior; `Unit` only for the bounded run-summary builder or ranking helper if introduced
|
||||||
|
- **Affected validation lanes**: `fast-feedback`, `confidence`
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: The feature is proven by operator-visible hierarchy, dominant-cause ordering, zero-output handling, and tenant-safe canonical run detail. That needs focused surface tests plus one narrow unit seam, not browser or heavy-governance breadth.
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `cd /Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`
|
||||||
|
- `cd /Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Monitoring/ArtifactTruthRunDetailTest.php tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php`
|
||||||
|
- `cd /Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php tests/Unit/Support/OperatorExplanation/OperatorExplanationBuilderTest.php`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: Shared fixture drift is the main risk. `BuildsGovernanceArtifactTruthFixtures` must stay opt-in, and any multi-cause seeded run helper should remain local to the Monitoring suite instead of becoming a repo-wide default.
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no; all new scenario builders must require explicit run type, outcome, reason codes, and related artifact context
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none
|
||||||
|
- **Surface-class relief / special coverage rule**: `monitoring-state-page` coverage is required; existing `standard-native-filament` relief is not enough for summary-order assertions on the canonical detail page
|
||||||
|
- **Closing validation and reviewer handoff**: Reviewers must confirm summary-first order, no duplicate dominant-cause copy across banners and decision zone, zero-output runs staying non-green, cross-family consistency for shared cause classes, and `404` vs `403` semantics on the canonical route.
|
||||||
|
- **Budget / baseline / trend follow-up**: Low-to-moderate assertion growth within Monitoring and one new focused suite; no lane-budget follow-up expected unless helper sprawl begins
|
||||||
|
- **Review-stop questions**: Does the change stay inside current Monitoring detail seams? Did any new summary helper become broader than this surface needs? Did shared fixtures remain opt-in? Did any touched view leak inaccessible tenant or artifact hints?
|
||||||
|
- **Escalation path**: document-in-feature unless a second shared semantic layer, new persistence, or broad fixture default is proposed; then reject-or-split
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
- **Why no dedicated follow-up spec is needed**: The expected suite cost and abstraction surface stay tightly bounded to one existing canonical detail page and its current governance run families
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/220-governance-run-summaries/
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── governance-run-summaries.logical.openapi.yaml
|
||||||
|
├── checklists/
|
||||||
|
│ └── requirements.md
|
||||||
|
└── tasks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Filament/
|
||||||
|
│ │ ├── Pages/
|
||||||
|
│ │ │ └── Operations/
|
||||||
|
│ │ │ └── TenantlessOperationRunViewer.php
|
||||||
|
│ │ └── Resources/
|
||||||
|
│ │ └── OperationRunResource.php
|
||||||
|
│ └── Support/
|
||||||
|
│ ├── OpsUx/
|
||||||
|
│ │ ├── OperationUxPresenter.php
|
||||||
|
│ │ ├── SummaryCountsNormalizer.php
|
||||||
|
│ │ ├── GovernanceRunDiagnosticSummary.php
|
||||||
|
│ │ └── GovernanceRunDiagnosticSummaryBuilder.php
|
||||||
|
│ ├── ReasonTranslation/
|
||||||
|
│ │ └── ReasonPresenter.php
|
||||||
|
│ └── Ui/
|
||||||
|
│ ├── EnterpriseDetail/
|
||||||
|
│ ├── GovernanceArtifactTruth/
|
||||||
|
│ │ └── ArtifactTruthPresenter.php
|
||||||
|
│ └── OperatorExplanation/
|
||||||
|
│ ├── OperatorExplanationBuilder.php
|
||||||
|
│ └── OperatorExplanationPattern.php
|
||||||
|
├── resources/
|
||||||
|
│ └── views/
|
||||||
|
│ └── filament/
|
||||||
|
│ └── pages/
|
||||||
|
│ └── operations/
|
||||||
|
│ └── tenantless-operation-run-viewer.blade.php
|
||||||
|
└── tests/
|
||||||
|
├── Feature/
|
||||||
|
│ ├── Authorization/
|
||||||
|
│ │ └── OperatorExplanationSurfaceAuthorizationTest.php
|
||||||
|
│ ├── Monitoring/
|
||||||
|
│ │ ├── ArtifactTruthRunDetailTest.php
|
||||||
|
│ │ ├── GovernanceOperationRunSummariesTest.php
|
||||||
|
│ │ └── GovernanceRunExplanationFallbackTest.php
|
||||||
|
│ ├── Filament/
|
||||||
|
│ │ └── OperationRunBaselineTruthSurfaceTest.php
|
||||||
|
│ └── RunAuthorizationTenantIsolationTest.php
|
||||||
|
└── Unit/
|
||||||
|
├── Support/
|
||||||
|
│ ├── OpsUx/
|
||||||
|
│ │ └── GovernanceRunDiagnosticSummaryBuilderTest.php
|
||||||
|
│ └── OperatorExplanation/
|
||||||
|
│ └── OperatorExplanationBuilderTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Standard Laravel monolith. The work stays concentrated in the current Monitoring detail files, existing `Support/OpsUx` and UI helper seams, and focused Pest suites. No new base directory, panel, or package is needed.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| Bounded run-summary helper/value object | Needed to keep dominant-cause ranking, affected-scale mapping, and summary ordering out of `OperationRunResource` and page templates | Extending the resource/page inline would bury operation-family logic in Filament schema code and make regression coverage brittle |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: Canonical governance run detail is still too technical for first-pass operator decisions, especially when execution succeeded but artifact usability did not, or when several degraded causes exist together.
|
||||||
|
- **Existing structure is insufficient because**: Existing badges, explanation patterns, and raw payload sections require operators to synthesize impact, trust, and next action themselves. The missing piece is a first-pass run-detail summary that ranks cause and scale for this single surface.
|
||||||
|
- **Narrowest correct implementation**: Add one run-detail-specific summary object and builder inside `Support/OpsUx`, derived entirely from `OperationRun`, `ArtifactTruthEnvelope`, `OperatorExplanationPattern`, and existing count/context payloads.
|
||||||
|
- **Ownership cost created**: One small builder/value-object pair, one local set of dominance rules, and focused Monitoring/unit tests.
|
||||||
|
- **Alternative intentionally rejected**: Page-local copy patches and ad-hoc Filament facts only. That would duplicate operation-type logic, make hierarchy drift likely, and fail to protect cross-family consistency.
|
||||||
|
- **Release truth**: Current-release truth. This plan improves an existing trust surface now rather than preparing a future platform abstraction.
|
||||||
|
|
||||||
|
## Phase 0 Research
|
||||||
|
|
||||||
|
Research outcomes are captured in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/220-governance-run-summaries/research.md`.
|
||||||
|
|
||||||
|
Key decisions:
|
||||||
|
|
||||||
|
- Keep canonical Monitoring run detail on `OperationRunResource` + `TenantlessOperationRunViewer`; do not create a second run-detail page.
|
||||||
|
- Treat `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, and `ReasonPresenter` as the canonical semantic inputs.
|
||||||
|
- Introduce one bounded `GovernanceRunDiagnosticSummary` seam so the decision zone can express affected scale, dominant-cause ranking, and secondary-cause detail without overloading the Filament resource schema.
|
||||||
|
- Derive affected-scale cues from existing `summary_counts`, run `context`, and related artifact metadata; do not add schema or `summary_counts` contract changes.
|
||||||
|
- Keep lifecycle/context banners specialized and let the decision zone own the dominant explanation to avoid duplicated operator copy.
|
||||||
|
- Extend current Monitoring and authorization suites and keep multi-cause fixture helpers local or opt-in.
|
||||||
|
|
||||||
|
## Phase 1 Design
|
||||||
|
|
||||||
|
Design artifacts are created under `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/220-governance-run-summaries/`:
|
||||||
|
|
||||||
|
- `research.md`: implementation-seam decisions, risks, and rejected alternatives
|
||||||
|
- `data-model.md`: logical model for the derived governance run summary, dominant-cause breakdown, and affected-scale cues
|
||||||
|
- `contracts/governance-run-summaries.logical.openapi.yaml`: internal logical contract for canonical operations list/detail rendering requirements
|
||||||
|
- `quickstart.md`: focused verification workflow for manual and automated validation
|
||||||
|
|
||||||
|
Design decisions:
|
||||||
|
|
||||||
|
- No schema migration is required; all summary state remains derived.
|
||||||
|
- The primary implementation seam is canonical run detail plus a small helper under `App\Support\OpsUx`, not a new cross-domain UI framework.
|
||||||
|
- Existing Filament action topology, route shape, authorization behavior, and destructive-action semantics remain unchanged.
|
||||||
|
- `OperationUxPresenter` remains the façade for memoized governance explanation state on run detail.
|
||||||
|
- Existing technical sections such as count diagnostics, failure payloads, evidence-gap detail, and artifact-truth detail remain available but must become secondary to the new summary block.
|
||||||
|
|
||||||
|
## Phase 1 Agent Context Update
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
- `.specify/scripts/bash/update-agent-context.sh copilot`
|
||||||
|
|
||||||
|
## Constitution Check — Post-Design Re-evaluation
|
||||||
|
|
||||||
|
- PASS — the design remains read-surface focused and does not introduce new write paths, Graph calls, assets, or authorization semantics.
|
||||||
|
- PASS — Livewire v4.0+ and Filament v5 constraints remain unchanged, no provider registration move is required, `OperationRunResource` remains non-searchable, and no new destructive actions or assets are introduced.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Phase A — Introduce One Bounded Governance Run Summary Seam
|
||||||
|
|
||||||
|
**Goal**: Derive one operator-first run-detail summary without creating a second truth source.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| A.1 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummary.php` | Add a small value object carrying headline, dominant cause, affected scale, trust statement, secondary causes, and next action |
|
||||||
|
| A.2 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Derive the summary from `OperationRun`, `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, `ReasonPresenter`, `summary_counts`, and run context |
|
||||||
|
| A.3 | `apps/platform/app/Support/OpsUx/OperationUxPresenter.php` | Reuse existing memoization to expose the summary on canonical run detail without adding a new cache family |
|
||||||
|
|
||||||
|
### Phase B — Rewire Canonical Run Detail Around The First Decision
|
||||||
|
|
||||||
|
**Goal**: Make the decision zone lead with humanized diagnostic summaries and push raw diagnostics down.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| B.1 | `apps/platform/app/Filament/Resources/OperationRunResource.php` | Update the enterprise-detail decision zone to render the new summary, affected-scale cue, and processing-versus-artifact split ahead of technical sections |
|
||||||
|
| B.2 | `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` | Keep scope, lifecycle, and restore banners specialized while removing duplicated dominant-cause copy from banner-level messaging |
|
||||||
|
| B.3 | `apps/platform/resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php` or existing enterprise-detail view partials | Ensure the default reading order is summary first, supporting facts second, diagnostics third |
|
||||||
|
|
||||||
|
### Phase C — Add Stable Rules For Covered Governance Run Families
|
||||||
|
|
||||||
|
**Goal**: Keep summary language and affected-scale cues stable across the five scoped governance families.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| C.1 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Add baseline-capture rules for blocked prerequisite, zero-subject capture, and unusable snapshot outcomes |
|
||||||
|
| C.2 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Add baseline-compare rules for suppressed output, ambiguous matches, evidence gaps, and strategy failures |
|
||||||
|
| C.3 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Add evidence snapshot, tenant review, and review-pack generation rules using existing related artifact truth plus run context |
|
||||||
|
| C.4 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Add one stable dominant-cause ranking rule so tied degraded runs do not reorder arbitrarily between renders |
|
||||||
|
|
||||||
|
### Phase D — Preserve Tenant Safety, Related Links, and Existing Action Topology
|
||||||
|
|
||||||
|
**Goal**: Improve explanation without changing route, RBAC, or action behavior.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| D.1 | `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php` | Keep canonical back-link lineage, active-tenant continuity, and grouped related navigation intact |
|
||||||
|
| D.2 | `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php` | Ensure summary output suppresses inaccessible artifact or tenant hints when related navigation is not allowed |
|
||||||
|
| D.3 | Existing authorization tests and related-link helpers | Keep `404` vs `403` semantics unchanged and verify no new mutation affordances appear |
|
||||||
|
|
||||||
|
### Phase E — Protect The Surface With Focused Regression Coverage
|
||||||
|
|
||||||
|
**Goal**: Add the smallest test set that locks summary order, multi-cause behavior, zero-output runs, and authorization safety.
|
||||||
|
|
||||||
|
| Step | File | Change |
|
||||||
|
|------|------|--------|
|
||||||
|
| E.1 | `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php` | Add unit coverage for dominant-cause ranking, affected-scale derivation, and next-step category mapping |
|
||||||
|
| E.2 | `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php` | Add end-to-end run-detail coverage for multi-cause degraded runs, all-zero runs, cross-family parity, and diagnostics-secondary ordering |
|
||||||
|
| E.3 | `apps/platform/tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php` and `apps/platform/tests/Feature/Monitoring/ArtifactTruthRunDetailTest.php` | Update existing assertions to match final summary-first wording and remove brittle duplication gaps |
|
||||||
|
| E.4 | `apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php` and `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php` | Extend canonical route coverage for tenant-safe summary rendering and inaccessible related navigation |
|
||||||
|
| E.5 | `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` plus focused Pest runs | Run formatting and the narrowest proving commands before implementation close-out |
|
||||||
|
|
||||||
|
## Key Design Decisions
|
||||||
|
|
||||||
|
### D-001 — Canonical run detail remains the only detailed run-inspection surface
|
||||||
|
|
||||||
|
The feature improves the current canonical Monitoring detail page instead of creating a second run viewer or a special governance-only route.
|
||||||
|
|
||||||
|
### D-002 — Existing truth and explanation envelopes remain canonical
|
||||||
|
|
||||||
|
`ArtifactTruthEnvelope` and `OperatorExplanationPattern` remain the semantic source of truth. The new summary layer only ranks and presents them for this one surface.
|
||||||
|
|
||||||
|
### D-003 — Affected scale stays derived from existing persisted signals
|
||||||
|
|
||||||
|
`summary_counts`, run `context`, failure summaries, and related artifact truth are sufficient inputs. The plan explicitly avoids schema changes or new count contracts.
|
||||||
|
|
||||||
|
### D-004 — Banners stay specialized; the decision zone owns the main explanation
|
||||||
|
|
||||||
|
Context, lifecycle, or restore-continuation banners may still appear, but the dominant cause and next-step explanation must live in the decision zone so the page does not say the same thing twice.
|
||||||
|
|
||||||
|
### D-005 — Shared fixtures stay opt-in
|
||||||
|
|
||||||
|
Multi-cause or zero-output scenario builders should remain local to the Monitoring suite unless a second real consumer proves they belong in a shared concern.
|
||||||
|
|
||||||
|
## Risk Assessment
|
||||||
|
|
||||||
|
| Risk | Impact | Likelihood | Mitigation |
|
||||||
|
|------|--------|------------|------------|
|
||||||
|
| The new summary duplicates existing banner copy and makes the page louder instead of calmer | High | Medium | Keep banners specialized and let the decision zone own dominant explanation text |
|
||||||
|
| Dominant-cause ranking changes arbitrarily between equivalent multi-cause runs | High | Medium | Encode one explicit ranking rule and cover it with unit tests plus one multi-cause feature test |
|
||||||
|
| Affected-scale cues drift by operation family and become inconsistent | Medium | Medium | Centralize scale mapping in the builder and reuse it across all covered run families |
|
||||||
|
| Shared fixtures or helper defaults silently hide required run context | Medium | Medium | Require explicit type, outcome, reason, and related artifact context in new scenario builders |
|
||||||
|
| Summary copy leaks inaccessible tenant or artifact hints on canonical `/admin` routes | High | Low | Keep authorization tests on related links and summary rendering together and suppress inaccessible context |
|
||||||
|
|
||||||
|
## Test Strategy
|
||||||
|
|
||||||
|
- Add one new focused feature suite for governance run summaries and keep it scoped to canonical Monitoring run detail.
|
||||||
|
- Add one narrow unit suite for dominant-cause and affected-scale derivation only if a dedicated builder is introduced.
|
||||||
|
- Reuse existing Monitoring and authorization suites for regression coverage instead of creating browser or heavy-governance breadth.
|
||||||
|
- Keep `BuildsGovernanceArtifactTruthFixtures` opt-in and add any multi-cause builder locally to the Monitoring suite first.
|
||||||
|
- Preserve DB-only rendering guarantees on canonical run detail while adjusting the visible summary hierarchy.
|
||||||
147
specs/220-governance-run-summaries/quickstart.md
Normal file
147
specs/220-governance-run-summaries/quickstart.md
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# Quickstart: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Validate that canonical governance operation run detail now answers the first operator question with one dominant summary, one short reason, one affected-scale cue where available, and one next step, while keeping raw diagnostics secondary and preserving current authorization and navigation semantics.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. Start Sail if it is not already running.
|
||||||
|
2. Ensure the acting user is a valid workspace member and is entitled to the target tenant where the run is tenant-bound.
|
||||||
|
3. Prepare representative runs for these cases:
|
||||||
|
- blocked baseline capture with no usable inventory basis
|
||||||
|
- baseline compare with ambiguous matches or evidence gaps
|
||||||
|
- evidence snapshot generation with stale or incomplete output
|
||||||
|
- tenant review composition with missing sections or stale evidence
|
||||||
|
- review-pack generation with internal-only or blocked outcome
|
||||||
|
- one multi-cause degraded run
|
||||||
|
- one zero-output or all-zero run that must not read as green
|
||||||
|
|
||||||
|
## Focused Automated Verification
|
||||||
|
|
||||||
|
Run formatting first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the smallest proving set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||||
|
tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php \
|
||||||
|
tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php \
|
||||||
|
tests/Feature/Monitoring/ArtifactTruthRunDetailTest.php \
|
||||||
|
tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php \
|
||||||
|
tests/Feature/RunAuthorizationTenantIsolationTest.php \
|
||||||
|
tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php \
|
||||||
|
tests/Unit/Support/OperatorExplanation/OperatorExplanationBuilderTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
If the new focused suite is not yet isolated, run the Monitoring subset instead:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Validation Pass
|
||||||
|
|
||||||
|
### 1. Canonical run detail entry path
|
||||||
|
|
||||||
|
Open `/admin/operations` and drill into a governance run.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- row navigation remains the inspect model,
|
||||||
|
- no new row or header action appears,
|
||||||
|
- and arriving from tenant context does not silently widen back to all-tenant semantics.
|
||||||
|
|
||||||
|
### 2. Baseline capture blocked by prerequisite
|
||||||
|
|
||||||
|
Open a blocked baseline-capture run.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- the page leads with `no baseline was captured`-style meaning,
|
||||||
|
- the missing prerequisite appears before raw payloads,
|
||||||
|
- execution status and artifact usability are visible as separate facts,
|
||||||
|
- and raw diagnostics remain lower on the page.
|
||||||
|
|
||||||
|
### 3. Baseline compare with ambiguity or suppressed output
|
||||||
|
|
||||||
|
Open a baseline-compare run with evidence gaps, ambiguous matches, or suppressed output.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- the first summary names the compare outcome and its trust limitation,
|
||||||
|
- the dominant cause is understandable without raw JSON,
|
||||||
|
- any affected-scale cue is visible when supported by stored counts or gap detail,
|
||||||
|
- and `0 findings` or zero-output does not read as an all-clear.
|
||||||
|
|
||||||
|
### 4. Evidence snapshot generation
|
||||||
|
|
||||||
|
Open a run that produced stale or incomplete evidence.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- processing success does not imply trustworthy evidence,
|
||||||
|
- the page states the evidence limitation before technical payloads,
|
||||||
|
- and next-step guidance points to the right recovery action.
|
||||||
|
|
||||||
|
### 5. Tenant review composition and review-pack generation
|
||||||
|
|
||||||
|
Open one review-compose run and one review-pack-generation run.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- review generation can explain missing sections or stale evidence without JSON,
|
||||||
|
- pack generation can explain internal-only or blocked shareability outcomes,
|
||||||
|
- and related artifact links remain available only when the actor is entitled to them.
|
||||||
|
|
||||||
|
### 6. Multi-cause degraded run
|
||||||
|
|
||||||
|
Open a run with two or more stored degraded causes.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- one dominant cause is shown first,
|
||||||
|
- at least one secondary cause is still discoverable,
|
||||||
|
- and the ordering is stable across reloads.
|
||||||
|
|
||||||
|
### 7. Cross-family parity
|
||||||
|
|
||||||
|
Open two covered governance runs from different families that share the same dominant cause class.
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- the same cause class keeps the same primary reading direction,
|
||||||
|
- the next-step category stays consistent where the persisted truth supports the same operator action,
|
||||||
|
- and cross-family wording does not drift into conflicting operator guidance.
|
||||||
|
|
||||||
|
### 8. Authorization and tenant safety
|
||||||
|
|
||||||
|
Confirm that:
|
||||||
|
|
||||||
|
- non-members still receive deny-as-not-found behavior,
|
||||||
|
- in-scope members lacking capability still receive `403` where expected,
|
||||||
|
- summary text does not leak inaccessible tenant or artifact hints,
|
||||||
|
- and `OperationRun` remains non-searchable.
|
||||||
|
|
||||||
|
### 9. Ten-second scan check
|
||||||
|
|
||||||
|
Timebox the first visible scan of one blocked, one degraded, and one zero-output governance run detail page.
|
||||||
|
|
||||||
|
Confirm that within 10-15 seconds an operator can determine:
|
||||||
|
|
||||||
|
- what happened,
|
||||||
|
- whether the resulting artifact is trustworthy enough to act on,
|
||||||
|
- what was affected when the stored data supports that cue,
|
||||||
|
- and what the next step is,
|
||||||
|
|
||||||
|
without opening diagnostic sections.
|
||||||
|
|
||||||
|
## Final Verification Notes
|
||||||
|
|
||||||
|
- Keep diagnostics present but secondary.
|
||||||
|
- Do not add retry, cancel, force-fail, or other intervention controls as part of this slice.
|
||||||
|
- If a manual reviewer sees the same dominant-cause copy both in a banner and in the decision zone, treat that as a regression and tighten the summary ownership.
|
||||||
49
specs/220-governance-run-summaries/research.md
Normal file
49
specs/220-governance-run-summaries/research.md
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
# Research: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
## Decision 1: Keep canonical governance run detail on the existing Monitoring viewer and detail resource
|
||||||
|
|
||||||
|
- **Decision**: Reuse `OperationRunResource` and `TenantlessOperationRunViewer` as the single canonical run-detail surface for Spec 220 instead of creating a new governance-only viewer.
|
||||||
|
- **Rationale**: The repo already routes canonical Monitoring run detail through these seams and already has the right RBAC, action-surface, and navigation guardrails in place. The problem is explanation order and summary quality, not missing routing or missing surface ownership.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Create a second governance-specific run-detail page. Rejected because it would duplicate route ownership, action hierarchy, and authorization semantics for one existing surface.
|
||||||
|
- Add page-local partials only in the Blade template. Rejected because the run-detail summary needs stable derivation rules, not just another rendering layer.
|
||||||
|
|
||||||
|
## Decision 2: Treat `ArtifactTruthPresenter`, `OperatorExplanationBuilder`, and `ReasonPresenter` as the canonical semantic inputs
|
||||||
|
|
||||||
|
- **Decision**: Build the new summary from the existing `ArtifactTruthEnvelope`, `OperatorExplanationPattern`, and reason-translation envelopes instead of introducing a second semantic source.
|
||||||
|
- **Rationale**: The repo already derives artifact truth and operator explanation for `OperationRun` records, including governance families like `baseline.capture`, `baseline.compare`, `tenant.evidence.snapshot.generate`, `tenant.review.compose`, and `tenant.review_pack.generate`. Reusing that chain preserves existing truth ownership and keeps the new work downstream and bounded.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Add a new persisted summary state to `operation_runs`. Rejected because the desired summary is fully derivable from current persisted truth and would create drift risk.
|
||||||
|
- Put all summary logic directly inside `OperationRunResource`. Rejected because it would bury operation-family rules inside Filament schema code and make tests brittle.
|
||||||
|
|
||||||
|
## Decision 3: Add one bounded `GovernanceRunDiagnosticSummary` seam only if affected-scale and dominant-cause rules cannot stay in the current presenter flow
|
||||||
|
|
||||||
|
- **Decision**: If the current detail seams cannot cleanly express dominant cause, affected scale, and secondary-cause breakdown, add one small value object plus builder under `App\Support\OpsUx` and expose it through `OperationUxPresenter`.
|
||||||
|
- **Rationale**: Spec 220 needs more than current badges and explanation labels. It needs one stable first-pass summary, especially for multi-cause degraded runs and all-zero runs. A small run-detail-specific helper is justified because the work is limited to one existing surface and several real operation families already consume the same route.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Extend `ArtifactTruthPresenter` to own all run-detail ranking logic. Rejected because artifact truth is broader than this one run-detail question and should remain canonical truth, not surface-specific emphasis logic.
|
||||||
|
- Build a generic cross-product explanation framework. Rejected because the spec is explicitly scoped to canonical governance run detail.
|
||||||
|
|
||||||
|
## Decision 4: Derive affected-scale cues from existing `summary_counts`, run context, and related artifact truth
|
||||||
|
|
||||||
|
- **Decision**: Affected scale must come from existing persisted signals such as `summary_counts`, known run-context payloads, failure summaries, and related artifact summaries. No schema change or count-contract expansion is planned.
|
||||||
|
- **Rationale**: Covered operation families already persist enough context to support statements like ambiguous subject matches, missing sections, partial evidence dimensions, or zero captured subjects. The missing work is ranking and presenting those signals consistently.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Add new operation-specific summary fields or nested count structures. Rejected because Ops-UX already constrains `summary_counts` to flat numeric keys, and the feature does not need new persistence.
|
||||||
|
- Omit affected-scale cues entirely. Rejected because the spec explicitly requires the page to explain what was affected, not just why it failed.
|
||||||
|
|
||||||
|
## Decision 5: Keep banners specialized and let the decision zone own the dominant explanation
|
||||||
|
|
||||||
|
- **Decision**: Existing canonical context, lifecycle, blocked-execution, and restore-continuation banners remain specialized. The main humanized summary must live in the decision zone so the page does not duplicate dominant-cause copy.
|
||||||
|
- **Rationale**: The current run detail already has banner-level messaging. Adding another banner or repeating the same explanation in two places would increase attention load instead of reducing it. The summary should become the first read inside the decision zone, with banners reserved for scope, stale lifecycle, and special restore continuity contexts.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Add a new top-of-page summary banner. Rejected because it would compete with existing lifecycle and context banners.
|
||||||
|
- Remove existing banners entirely. Rejected because they already communicate valid scope or lifecycle information outside the core diagnostic summary.
|
||||||
|
|
||||||
|
## Decision 6: Extend current Monitoring and authorization suites and keep multi-cause fixtures local first
|
||||||
|
|
||||||
|
- **Decision**: Reuse existing Monitoring, Filament, and authorization suites; add one new focused `GovernanceOperationRunSummariesTest` plus one narrow unit seam if a builder is introduced. Keep multi-cause fixture builders local to the Monitoring suite unless another consumer emerges.
|
||||||
|
- **Rationale**: The repo already has substantial run-detail coverage, including hierarchy assertions, artifact-truth rendering, and `404` vs `403` semantics. The main gaps are multi-cause degraded runs, all-zero runs, and cross-family consistency. Those gaps can be covered without creating a new heavy or browser test family.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Rely mainly on browser tests. Rejected because the current feature is better proven through existing Livewire and feature suites.
|
||||||
|
- Move multi-cause builders into shared fixture concerns immediately. Rejected because only Spec 220 currently needs those seeds and shared defaults would be risky.
|
||||||
238
specs/220-governance-run-summaries/spec.md
Normal file
238
specs/220-governance-run-summaries/spec.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
# Feature Specification: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
**Feature Branch**: `220-governance-run-summaries`
|
||||||
|
**Created**: 2026-04-20
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Humanized Diagnostic Summaries for Governance Operations"
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: Governance operation run-detail pages already carry correct outcome, reason, and artifact-truth semantics, but the first useful explanation still often lives in raw JSON or low-level diagnostic sections.
|
||||||
|
- **Today's failure**: An operator can open a run that reads `Completed with follow-up`, `Partial`, or `Blocked` and still has to infer the real business meaning: what was affected, which cause dominates, whether retry or resume helps, and whether the resulting artifact is trustworthy enough to act on.
|
||||||
|
- **User-visible improvement**: Governance run detail leads with one human-readable summary that explains impact, dominant cause, artifact trustworthiness, and next action before any raw diagnostics.
|
||||||
|
- **Smallest enterprise-capable version**: Add one bounded humanized summary layer to canonical governance run detail only, reusing existing outcome taxonomy, reason translation, artifact-truth semantics, and explanation patterns without changing persistence, lifecycle ownership, or action inventory.
|
||||||
|
- **Explicit non-goals**: No operations-list redesign, no dashboard overhaul, no new persistence for summaries, no removal of raw JSON, no new remediation controls on run detail, and no generalized rewrite of every governance artifact page.
|
||||||
|
- **Permanent complexity imported**: One derived governance-run summary contract, one dominant-cause presentation rule set for multi-cause degraded runs, and focused regression coverage for cross-family consistency.
|
||||||
|
- **Why now**: The roadmap marks this as the next open adoption slice after Spec 214. Specs 156, 157, 158, 161, and 214 already established the language and truth model; leaving run detail technical would keep a core trust surface lagging behind the foundation work.
|
||||||
|
- **Why not local**: A page-local copy cleanup would recreate divergent run-detail dialects across baseline, evidence, review, and review-pack governance runs and would not reliably separate processing success from artifact usability.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: One red flag: a reusable guidance pattern across multiple governance run families. It remains acceptable because the scope is restricted to one existing canonical detail surface and does not add new persisted truth, new states, or a cross-product framework.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: canonical-view
|
||||||
|
- **Primary Routes**: `/admin/operations`, `/admin/operations/{run}`
|
||||||
|
- **Data Ownership**: Tenant-bound governance `OperationRun` records remain tenant-owned operational artifacts exposed through the canonical Monitoring route. Related baseline snapshots can stay workspace-owned, while evidence snapshots, tenant reviews, and review packs remain tenant-owned. This feature changes interpretation and ordering on the canonical run-detail surface only.
|
||||||
|
- **RBAC**: Workspace membership is required for Monitoring access. Tenant entitlement is still required before revealing tenant-bound governance runs or related artifact links from the canonical route. Existing monitoring-view and related-artifact authorization rules remain authoritative. Non-members or non-entitled users remain deny-as-not-found. Members who can reach Monitoring but lack an existing related action permission remain authorization failures for that action.
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: When a user reaches Monitoring from an active tenant context, the operations list and related links continue to preserve that tenant context. Opening a governance run detail must not silently broaden the operator back to all tenants.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Humanized summaries, dominant-cause labels, affected-scale cues, and related artifact links are only rendered after workspace and tenant entitlement checks succeed for the referenced run. Inaccessible tenant-bound runs and related records behave as not found and must not leak artifact names, tenant names, or result hints.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Canonical Monitoring operation run detail for governance operations | yes | Native Filament + existing Monitoring detail primitives | shared governance run-detail family | detail, summary hierarchy, diagnostics hierarchy | yes | Existing diagnostic-surface exception remains; this slice only makes the first read operator-safe |
|
||||||
|
|
||||||
|
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Canonical Monitoring operation run detail for governance operations | Tertiary Evidence / Diagnostics Surface | After drilling in from a baseline, evidence, review, or pack workflow, the operator needs to understand what actually happened and what to do next | Dominant artifact impact, dominant cause, affected scale, processing-versus-artifact split, and next action | Raw JSON, complete reason-code detail, provider payloads, low-level counters, and full multi-cause evidence | Not primary because operators should usually arrive here after another surface already identified the case; this page is the deep explanation layer | Follows drill-in from governance artifact and Monitoring workflows instead of becoming a new queue | Removes the need to read badges and raw JSON before understanding the real problem |
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Canonical Monitoring operation run detail for governance operations | Record / Detail / Actions | Canonical diagnostic detail | Open the related artifact or return to the source workflow with the correct next step | Explicit operation-run detail page | forbidden | Existing related navigation remains in header or contextual detail sections | none | /admin/operations | /admin/operations/{run} | Workspace context, active tenant context when present, related artifact type, run family | Operation runs / Operation run | Dominant artifact impact, dominant cause, affected scale, and next action before raw diagnostics | diagnostic_exception - canonical run detail remains the deepest evidence surface, so raw diagnostics stay present, but they must no longer lead the page |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Canonical Monitoring operation run detail for governance operations | Workspace manager or entitled tenant operator | Diagnose why a governance run produced a trustworthy, limited, blocked, or unusable artifact and decide the correct follow-up | Canonical detail | What happened, how much was affected, can I trust the resulting artifact, and what should I do next? | Dominant artifact-impact statement, dominant cause, affected scale, processing-versus-artifact split, next-step guidance, and related artifact context | Raw JSON, full reason-code inventory, provider payloads, low-level counters, and complete multi-cause diagnostics | execution outcome, artifact usability, completeness or reliability, dominant cause, actionability | None on this page; any linked mutations keep their original mutation scopes on their native surfaces | Open related artifact, inspect diagnostics | none |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: no
|
||||||
|
- **New persisted entity/table/artifact?**: no
|
||||||
|
- **New abstraction?**: yes
|
||||||
|
- **New enum/state/reason family?**: no
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no
|
||||||
|
- **Current operator problem**: Run detail is semantically correct but still too technical for first-pass operator decisions, which allows false-green or ambiguous readings on a core governance troubleshooting surface.
|
||||||
|
- **Existing structure is insufficient because**: Existing badges, reason translation, and raw diagnostic payloads still force operators to synthesize impact, trust, and next action themselves. Local copy tweaks would drift by run family and would not reliably separate execution throughput from artifact trustworthiness.
|
||||||
|
- **Narrowest correct implementation**: Add one bounded summary contract for governance operation run detail only, derived from the existing truth and explanation foundations, while preserving all diagnostics beneath it.
|
||||||
|
- **Ownership cost**: Ongoing maintenance of one shared summary mapping, one stable dominant-cause breakdown rule set, and focused regression coverage for the covered governance run families.
|
||||||
|
- **Alternative intentionally rejected**: Per-page copy patches and a broader operations redesign. The first is too weak and inconsistent; the second is unnecessary for the current operator problem.
|
||||||
|
- **Release truth**: Current-release truth. This spec makes an existing trust surface readable now instead of preparing a future architecture layer.
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
|
||||||
|
|
||||||
|
Canonical replacement is preferred over preservation.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Feature
|
||||||
|
- **Validation lane(s)**: fast-feedback, confidence
|
||||||
|
- **Why this classification and these lanes are sufficient**: The change is proven by what operators see on the canonical Monitoring run-detail page. Focused feature coverage over seeded governance run scenarios is sufficient to prove explanation hierarchy, cause breakdown, and authorization safety without introducing browser or heavy-governance breadth.
|
||||||
|
- **New or expanded test families**: Expand Monitoring feature coverage for governance run detail across baseline capture, baseline compare, evidence snapshot generation (`tenant.evidence.snapshot.generate`), tenant review composition (`tenant.review.compose`), and review-pack generation (`tenant.review_pack.generate`). Add one positive and one negative authorization case for tenant-bound governance runs on the canonical route.
|
||||||
|
- **Fixture / helper cost impact**: Low-to-moderate. Tests can reuse existing workspace, tenant, entitlement, and `OperationRun` setup, but need explicit seeded cases where execution outcome and artifact usability diverge, plus multi-cause degraded runs.
|
||||||
|
- **Heavy-family visibility / justification**: none
|
||||||
|
- **Special surface test profile**: monitoring-state-page
|
||||||
|
- **Standard-native relief or required special coverage**: Ordinary feature coverage is sufficient, with explicit assertions that summary-first hierarchy appears before raw diagnostics and that multi-cause degraded runs stay human-readable.
|
||||||
|
- **Reviewer handoff**: Reviewers must confirm that run detail leads with one dominant explanation, that processing success never reads as automatic artifact success, that raw JSON remains secondary, that a positive and negative authorization case exist, and that the proof stays inside focused Monitoring feature coverage.
|
||||||
|
- **Budget / baseline / trend impact**: Low increase in Monitoring feature assertions only; no new heavy or browser baseline is expected.
|
||||||
|
- **Escalation needed**: none
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Monitoring/ArtifactTruthRunDetailTest.php tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php tests/Feature/RunAuthorizationTenantIsolationTest.php`
|
||||||
|
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php tests/Unit/Support/OperatorExplanation/OperatorExplanationBuilderTest.php`
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Understand the dominant problem fast (Priority: P1)
|
||||||
|
|
||||||
|
An operator opens a governance run detail page and needs to understand the dominant problem and next step without reading raw JSON.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core trust outcome. If the first read remains technical, the feature has not delivered its value.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by opening seeded governance runs on the canonical Monitoring detail route and verifying that an operator can identify what happened and what to do next from the default-visible summary alone.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a baseline compare run completed with follow-up because subject matching was ambiguous, **When** an operator opens the run detail page, **Then** the page states that the compare finished but the result is only partially trustworthy, names ambiguous matching as the dominant cause, and points the operator to scope review before any raw diagnostics.
|
||||||
|
2. **Given** a baseline capture run is blocked because no usable inventory basis exists, **When** an operator opens the run detail page, **Then** the page states that no baseline was captured, explains the missing prerequisite, and points to the prerequisite action before any raw JSON.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Separate processing success from artifact trust (Priority: P2)
|
||||||
|
|
||||||
|
An operator needs technically successful processing counts to remain visibly separate from whether the resulting artifact is usable, shareable, or decision-grade.
|
||||||
|
|
||||||
|
**Why this priority**: False-green interpretations come from execution success reading like artifact success.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by reviewing governance runs where processing completed but the resulting artifact stayed stale, limited, internal-only, or otherwise not decision-grade.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an evidence snapshot generation run processed records successfully but produced a stale or incomplete snapshot, **When** an operator opens run detail, **Then** the page shows processing success separately from evidence usability and does not headline the run as unconditional success.
|
||||||
|
2. **Given** a review-pack generation run completed technically but the resulting pack is only suitable for internal follow-up, **When** an operator opens run detail, **Then** the page explains the pack outcome separately from the run completion state and names the correct follow-up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Read multi-cause degraded runs without flattening (Priority: P3)
|
||||||
|
|
||||||
|
An operator needs a degraded governance run with several contributing causes to stay understandable without collapsing into one vague abstract state.
|
||||||
|
|
||||||
|
**Why this priority**: Multi-cause degraded runs are where operator trust collapses fastest if the detail page is too generic.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by opening a seeded multi-cause degraded governance run and verifying that the page names one dominant cause first while preserving additional cause context in a secondary breakdown.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a tenant review generation run is limited by stale evidence and missing sections, **When** an operator opens run detail, **Then** the page shows one dominant cause with affected scale, preserves the second cause in secondary detail, and provides a next step that matches the dominant blocker.
|
||||||
|
2. **Given** a governance run contains both retryable and structural issues, **When** an operator opens run detail, **Then** the default summary distinguishes the dominant follow-up path instead of flattening all causes into one generic inspection message.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- A governance run can complete technically and still leave no decision-grade artifact. The page must explain that divergence directly instead of treating all-zero or fully processed counters as an all-clear.
|
||||||
|
- A governance run can have no persisted related artifact because input was missing or output was intentionally suppressed. The summary must explain the absence without requiring a raw payload.
|
||||||
|
- Multiple causes can have similar scale. The page must apply one stable dominant-cause rule so summary ordering does not become arbitrary between otherwise equivalent runs.
|
||||||
|
- Raw diagnostics can be unavailable, collapsed, or intentionally deferred. The first-pass summary must remain understandable from the persisted run truth alone.
|
||||||
|
- Scheduled or system-initiated governance runs can appear on the same page. The summary must stay humanized without implying that terminal user notifications or interactive start flows changed.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature does not introduce new Microsoft Graph calls, new mutation flows, or new scheduled or queued work. It changes the explanation hierarchy on the canonical Monitoring detail surface for already persisted governance runs.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature introduces one bounded interpretation layer because direct mapping from existing outcome badges, reason labels, and raw context still forces operators to synthesize trust and next action themselves. A narrower per-family copy fix is insufficient because the same governance run families would drift apart. No new persistence, state family, or artifact truth source is added.
|
||||||
|
|
||||||
|
**Constitution alignment (TEST-GOV-001):** Proof remains in focused feature coverage for Monitoring run detail. No new heavy-governance or browser family is required. Fixture cost stays explicit and limited to seeded run scenarios where execution outcome, artifact usability, and dominant cause differ.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-UX):** Existing `OperationRun` lifecycle rules remain unchanged. The feature does not change the three feedback surfaces, does not change `OperationRun.status` or `OperationRun.outcome` ownership, and does not introduce new `summary_counts` keys or non-numeric summary values. Scheduled or system-run behavior remains unchanged, including initiator-null notification rules. New regression guards focus on run-detail explanation order and summary-count meaning, not lifecycle transitions.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** The affected authorization plane is the workspace-admin `/admin` Monitoring plane with tenant-entitlement enforcement for tenant-bound governance runs. Non-members or non-entitled viewers continue to receive 404. Members who can reach Monitoring but lack a currently required related action permission continue to receive 403 for that action. Existing server-side authorization remains authoritative for related artifact links and any linked mutation surfaces. Global search behavior is unchanged; `OperationRun` remains non-searchable and tenant-safe.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-EX-AUTH-001):** No `/auth/*` behavior is introduced or broadened by this feature.
|
||||||
|
|
||||||
|
**Constitution alignment (BADGE-001):** Any changed status emphasis on run detail continues to use centralized outcome, reason, and artifact-truth semantics. This feature changes ordering and explanation, not badge ownership or ad-hoc color rules.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-FIL-001):** The feature reuses native Filament detail primitives, sections, infolist-style summary areas, and existing Monitoring detail components. Local replacement markup for status language is intentionally avoided. Semantic emphasis stays in shared truth primitives and summary ordering rather than page-local color or border rules.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-NAMING-001):** The target object is the operation run. Primary summary language uses operator-facing terms such as completed with follow-up, blocked by prerequisite, partially trustworthy result, stale evidence basis, or internal-only pack outcome. Implementation-first terms such as raw reason-code slugs, payload keys, or support-tier labels remain secondary diagnostics only.
|
||||||
|
|
||||||
|
**Constitution alignment (DECIDE-001):** The affected surface remains a Tertiary Evidence / Diagnostics Surface. It does not become a new primary queue. Its human-in-the-loop purpose is to make one drilled-in governance case understandable without further reconstruction. Immediate visibility must include impact, dominant cause, trust direction, and next action. Raw diagnostics remain preserved but explicitly secondary.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-CONST-001 / UI-SURF-001 / ACTSURF-001 / UI-HARD-001 / UI-EX-001 / UI-REVIEW-001 / HDR-001):** The chosen action-surface class is record/detail because the operator is already inside one explicit run. The most likely next action is to open the related artifact or return to the source workflow with the correct next step. The one primary inspect model remains the existing operation-run detail page. There is no row click on the detail surface. Pure navigation stays in existing related links and does not compete with mutation. No destructive actions are added. Canonical routes remain `/admin/operations` and `/admin/operations/{run}`. Scope signals remain workspace context, tenant context when relevant, and related artifact family. The canonical noun remains `Operation run`.
|
||||||
|
|
||||||
|
**Constitution alignment (ACTSURF-001 - action hierarchy):** No header, row, bulk, or workbench action inventory changes are introduced. The feature must not use explanation hardening as a backdoor to add retry, cancel, force-fail, or other intervention controls.
|
||||||
|
|
||||||
|
**Constitution alignment (OPSURF-001):** Default-visible content on `/admin/operations/{run}` must stay operator-first. Diagnostics are secondary and explicitly revealed below the primary summary. Status dimensions must stay distinct: execution outcome, artifact usability, dominant cause, and next-step category. Workspace and tenant context remain visible in the existing Monitoring detail shell. Any linked mutation continues to communicate its scope on the native surface where it lives.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-SEM-001 / LAYER-001 / TEST-TRUTH-001):** Direct mapping from canonical run truth to UI is insufficient because current badges and raw payloads still require operator interpretation. This feature adds one bounded run-summary layer and does not introduce redundant truth across models, service results, presenters, wrappers, or persisted mirrors. Tests focus on business consequences: first-pass understanding, no false-green reading, and consistent next-step guidance.
|
||||||
|
|
||||||
|
**Constitution alignment (Filament Action Surfaces):** The feature modifies a Filament-backed detail surface and therefore includes a UI Action Matrix. The Action Surface Contract remains satisfied: exactly one primary inspect model exists, redundant `View` actions remain absent, empty action groups remain absent, and no destructive placement changes occur. UI-FIL-001 is satisfied with the existing diagnostic-surface exception retained.
|
||||||
|
|
||||||
|
**Constitution alignment (UX-001 — Layout & Information Architecture):** The affected screen remains a structured detail page. Humanized summary content must live in deliberate summary sections ahead of diagnostics, not as scattered helper text. No create or edit layout changes are introduced, and no UX-001 exemption is needed beyond the already accepted diagnostic detail nature of the page.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-220-001**: The system MUST derive a humanized governance-run summary from existing run outcome, reason translation, artifact-truth, and explanation inputs without creating a new persisted truth source.
|
||||||
|
- **FR-220-002**: Canonical governance run detail MUST lead with exactly one dominant artifact-impact statement, one short supporting reason, one next-step category, and one affected-scale cue in the default-visible summary area.
|
||||||
|
- **FR-220-003**: Governance run detail MUST keep processing success and throughput counts visibly separate from resulting artifact usability, trustworthiness, shareability, or decision-readiness.
|
||||||
|
- **FR-220-004**: For multi-cause degraded governance runs, the detail page MUST identify one dominant cause first and preserve additional causes in a secondary breakdown instead of flattening them into one generic state.
|
||||||
|
- **FR-220-005**: Next-step guidance on governance run detail MUST distinguish at least retry later, resume capture or generation, refresh prerequisite data, review scope or ambiguous matches, manually validate, and no further action when the persisted truth supports those distinctions.
|
||||||
|
- **FR-220-006**: Raw JSON, raw reason-code inventories, provider payloads, and low-level counters MUST remain available on governance run detail but MUST not be the first explanatory block.
|
||||||
|
- **FR-220-007**: The same cause class across covered governance run families MUST render with the same primary reading direction and next-step category on canonical run detail.
|
||||||
|
- **FR-220-008**: The first implementation slice MUST cover governance runs for baseline capture, baseline compare, evidence snapshot generation (`tenant.evidence.snapshot.generate`), tenant review composition (`tenant.review.compose`), and review-pack generation (`tenant.review_pack.generate`).
|
||||||
|
- **FR-220-009**: A governance run that completed technically but produced a degraded, blocked, stale, internal-only, or otherwise non-decision-grade artifact MUST explain that divergence explicitly and MUST NOT headline as unconditional success.
|
||||||
|
- **FR-220-010**: All-zero or zero-output governance runs MUST explain why no decision-grade result exists and MUST NOT read as neutral or implicit all-clear.
|
||||||
|
- **FR-220-011**: Humanized summaries, affected-scale cues, and related artifact links on canonical Monitoring run detail MUST remain tenant-safe and must not leak inaccessible tenant context or artifact hints.
|
||||||
|
- **FR-220-012**: This feature MUST NOT introduce new `OperationRun` statuses, outcomes, reason-code families, `summary_counts` keys, notification surfaces, or run-detail intervention controls.
|
||||||
|
- **FR-220-013**: Existing action inventory on operation-run detail MUST remain unchanged; humanized summaries must not add retry, cancel, force-fail, or other mutation controls.
|
||||||
|
- **FR-220-014**: Primary summary vocabulary on governance run detail MUST use the shared operator language established by Specs 156, 157, 158, 161, and 214 rather than implementation-first labels.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Canonical Monitoring operation run detail for governance operations | `apps/platform/app/Filament/Pages/Monitoring/Operations.php`; `apps/platform/app/Filament/Resources/OperationRunResource.php` | none added | Existing explicit navigation from the operations list or related links remains the only inspect model | none added | none | n/a | Existing related-artifact navigation remains; no new action labels introduced by this feature | n/a | no new audit behavior | Action Surface Contract remains satisfied. No redundant `View` action, no empty action groups, no destructive change. Existing diagnostic exception remains, but summary-first hierarchy becomes mandatory. |
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Humanized Governance Run Summary**: A derived first-pass summary for one governance operation run containing the dominant artifact impact, short reason, affected scale, and next-step direction.
|
||||||
|
- **Dominant Cause Breakdown**: A derived secondary explanation that preserves additional causes when a governance run is degraded for more than one reason.
|
||||||
|
- **Artifact Impact Statement**: The operator-facing truth about whether the resulting artifact is trustworthy, limited, blocked, internal-only, stale, or otherwise unsuitable for immediate reliance, separate from execution success.
|
||||||
|
|
||||||
|
## Assumptions & Dependencies
|
||||||
|
|
||||||
|
- Specs 156, 157, 158, 161, and 214 remain the authoritative foundations for operator vocabulary, reason translation, artifact-truth semantics, explanation patterns, and governance-surface compression.
|
||||||
|
- The canonical Monitoring run viewer from Spec 144 remains the existing detail surface and data-access contract for this slice.
|
||||||
|
- Covered governance run families already persist enough reason and outcome data to drive a first-pass summary without adding new persistence.
|
||||||
|
- This spec intentionally stays on run detail and does not pull surrounding artifact list or detail surfaces back into scope.
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Redesign the operations list, Monitoring landing page, or dashboard attention surfaces.
|
||||||
|
- Add retry, cancel, force-fail, or reconcile-now controls to run detail.
|
||||||
|
- Remove raw JSON or low-level diagnostics from the run-detail page.
|
||||||
|
- Create a new lifecycle or status model for `OperationRun`.
|
||||||
|
- Expand the slice to every non-governance run family.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-220-001**: In seeded acceptance review, an operator can determine within 15 seconds from the canonical governance run-detail page what happened, whether the resulting artifact is trustworthy enough to act on, and what the next step is without opening raw diagnostics.
|
||||||
|
- **SC-220-002**: In automated coverage, 100% of covered scenarios where execution success diverges from artifact trust show those truths as separate visible statements with no contradictory headline.
|
||||||
|
- **SC-220-003**: In automated coverage, 100% of covered multi-cause degraded governance runs show one dominant cause first and preserve at least one additional cause in secondary detail.
|
||||||
|
- **SC-220-004**: In acceptance review and regression tests, raw JSON and low-level diagnostics are never the first explanatory block on the run-detail page for any covered governance run family.
|
||||||
146
specs/220-governance-run-summaries/tasks.md
Normal file
146
specs/220-governance-run-summaries/tasks.md
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
# Tasks: Humanized Diagnostic Summaries for Governance Operations
|
||||||
|
|
||||||
|
**Input**: Design documents from `/specs/220-governance-run-summaries/`
|
||||||
|
**Prerequisites**: `plan.md`, `spec.md`, `research.md`, `data-model.md`, `contracts/governance-run-summaries.logical.openapi.yaml`, `quickstart.md`
|
||||||
|
|
||||||
|
**Tests**: Required. This feature changes runtime behavior on a Filament-backed Monitoring detail surface, so Pest feature and unit coverage must ship with the implementation.
|
||||||
|
|
||||||
|
**Test Governance Checklist**
|
||||||
|
|
||||||
|
- Lane assignment stays `fast-feedback` plus `confidence` and remains the narrowest sufficient proof for this surface change.
|
||||||
|
- New tests stay in focused Monitoring and unit suites; no heavy-governance or browser family is introduced.
|
||||||
|
- Shared helpers and fixtures remain opt-in, especially `BuildsGovernanceArtifactTruthFixtures`.
|
||||||
|
- Validation commands stay limited to the focused run-detail suites listed in `specs/220-governance-run-summaries/quickstart.md`.
|
||||||
|
- The declared surface profile remains `monitoring-state-page`.
|
||||||
|
- Any budget or escalation note stays inside this feature instead of becoming a follow-up spec.
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Test Scaffolding)
|
||||||
|
|
||||||
|
**Purpose**: Create the focused test seams and fixture hooks the implementation will use.
|
||||||
|
|
||||||
|
- [X] T001 [P] Create the focused canonical run-detail feature suite and local scenario helpers for zero-output and multi-cause runs in `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`
|
||||||
|
- [X] T002 [P] Create the focused summary-derivation unit suite in `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php`
|
||||||
|
- [X] T003 [P] Extend only generic opt-in shared governance fixture builders for blocked, stale, and internal-only artifact cases in `apps/platform/tests/Feature/Concerns/BuildsGovernanceArtifactTruthFixtures.php`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Establish the shared derived-summary seam that all user stories build on.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work should start until this phase is complete.
|
||||||
|
|
||||||
|
- [X] T004 Create the derived summary value object in `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummary.php`
|
||||||
|
- [X] T005 Create the shared summary builder with canonical `OperationRun`, artifact-truth, reason, and explanation inputs in `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php`
|
||||||
|
- [X] T006 Wire memoized governance summary access into `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`
|
||||||
|
- [X] T007 [P] Add guard coverage that summary derivation preserves canonical `summary_counts` meaning and does not invent new count keys in `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php`
|
||||||
|
- [X] T008 [P] Extend canonical operator-language assertions and explicit next-step category matrix coverage for `retry later`, `resume capture or generation`, `refresh prerequisite data`, `review scope or ambiguous matches`, `manually validate`, and `no further action` in `apps/platform/tests/Unit/Support/OperatorExplanation/OperatorExplanationBuilderTest.php` and `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: The shared summary seam exists, is memoized through the current Ops UX presenter, and is guarded against count-contract drift.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Understand the dominant problem fast (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Make the canonical governance run-detail page explain the dominant problem, affected scale, and next step before any raw diagnostics.
|
||||||
|
|
||||||
|
**Independent Test**: Open seeded baseline-capture and baseline-compare runs on `/admin/operations/{run}` and confirm the default-visible summary answers what happened and what to do next without opening diagnostic sections.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [X] T009 [P] [US1] Add feature scenarios for baseline-capture and baseline-compare summary-first hierarchy, no new header actions, and zero-output messaging in `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`
|
||||||
|
- [X] T010 [P] [US1] Add unit cases for dominant headline, supporting reason, affected-scale cue, and next-step selection for baseline-capture and baseline-compare runs in `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [X] T011 [US1] Implement `baseline.capture` and `baseline.compare` summary mappings in `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php`
|
||||||
|
- [X] T012 [US1] Expose baseline summary facts through the memoized presenter API in `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`
|
||||||
|
- [X] T013 [US1] Render the default-visible summary block before technical diagnostics in `apps/platform/app/Filament/Resources/OperationRunResource.php`
|
||||||
|
- [X] T014 [US1] Keep canonical context, lifecycle, and restore banners specialized without duplicating the dominant explanation in `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||||
|
- [X] T015 [US1] Preserve summary-first page-shell order for canonical run detail in `apps/platform/resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php`
|
||||||
|
- [X] T016 [US1] Update summary fallback expectations for the new first-read hierarchy in `apps/platform/tests/Feature/Monitoring/GovernanceRunExplanationFallbackTest.php`
|
||||||
|
- [X] T017 [US1] Update run-detail hierarchy assertions so diagnostics stay secondary in `apps/platform/tests/Feature/Monitoring/ArtifactTruthRunDetailTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Baseline capture and baseline compare runs are readable from the summary block alone, with diagnostics preserved but no longer leading the page.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Separate processing success from artifact trust (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Keep execution completion visible while clearly separating whether the resulting artifact is trustworthy, limited, stale, or internal-only.
|
||||||
|
|
||||||
|
**Independent Test**: Open seeded evidence-snapshot and review-pack runs where processing completed but the artifact is not decision-grade, and confirm the page shows those truths as separate visible statements.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [X] T018 [P] [US2] Add feature scenarios for evidence-snapshot and review-pack runs that separate processing completion from artifact trust in `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`
|
||||||
|
- [X] T019 [P] [US2] Add regression assertions for execution-outcome versus artifact-impact separation in `apps/platform/tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php`
|
||||||
|
- [X] T020 [P] [US2] Add positive and negative authorization coverage for tenant-safe summary rendering and related links in `apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [X] T021 [US2] Implement `tenant.evidence.snapshot.generate` and `tenant.review_pack.generate` summary mappings with distinct execution and artifact-impact facts in `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php`
|
||||||
|
- [X] T022 [US2] Render separated execution outcome and artifact-impact facts in `apps/platform/app/Filament/Resources/OperationRunResource.php`
|
||||||
|
- [X] T023 [US2] Keep related artifact navigation and tenant-context continuity aligned with summary copy in `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||||
|
- [X] T024 [US2] Extend canonical route isolation assertions for deny-as-not-found and in-scope `403` behavior in `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: A technically completed run can no longer read like unconditional success when the artifact itself is stale, limited, or internal-only.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Read multi-cause degraded runs without flattening (Priority: P3)
|
||||||
|
|
||||||
|
**Goal**: Keep degraded governance runs understandable by showing one dominant cause first while preserving secondary causes and affected-scale context.
|
||||||
|
|
||||||
|
**Independent Test**: Open a seeded multi-cause tenant-review run on `/admin/operations/{run}` and confirm the page shows one dominant cause first, preserves secondary causes, and keeps the same ordering across reloads.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [X] T025 [P] [US3] Add feature scenarios for tenant-review multi-cause degraded runs, stable dominant-cause ordering, and cross-family parity for the same cause class across at least two covered governance families in `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`
|
||||||
|
- [X] T026 [P] [US3] Add unit cases for dominant-cause ranking, secondary causes, and affected-scale confidence in `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php`
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [X] T027 [US3] Implement `tenant.review.compose` multi-cause summary mapping and shared ranking rules across covered governance families in `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php`
|
||||||
|
- [X] T028 [US3] Render secondary-cause breakdown and affected-scale detail without flattening the dominant explanation in `apps/platform/app/Filament/Resources/OperationRunResource.php`
|
||||||
|
- [X] T029 [US3] Suppress inaccessible tenant and artifact hints in summary text and related-navigation branches in `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php`
|
||||||
|
- [X] T030 [US3] Keep canonical run-detail banners and page-shell copy free of duplicated multi-cause messaging in `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`
|
||||||
|
- [X] T031 [US3] Extend authorization surface assertions so inaccessible related context never leaks through summary or navigation output in `apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Multi-cause degraded runs stay human-readable, deterministically ordered, and tenant-safe.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Final guardrail review, formatting, focused validation, and manual smoke.
|
||||||
|
|
||||||
|
- [X] T032 [P] Review monitoring-state-page guardrail coverage, lane assignment, and fixture-cost notes against `specs/220-governance-run-summaries/plan.md` and `specs/220-governance-run-summaries/quickstart.md`
|
||||||
|
- [X] T033 [P] Format changed PHP and Blade files including `apps/platform/app/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilder.php`, `apps/platform/app/Filament/Resources/OperationRunResource.php`, `apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php`, and `apps/platform/resources/views/filament/pages/operations/tenantless-operation-run-viewer.blade.php`
|
||||||
|
- [X] T034 Run the canonical proving commands for `apps/platform/tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php`, `apps/platform/tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php`, `apps/platform/tests/Feature/Monitoring/ArtifactTruthRunDetailTest.php`, `apps/platform/tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php`, `apps/platform/tests/Feature/RunAuthorizationTenantIsolationTest.php`, `apps/platform/tests/Unit/Support/OpsUx/GovernanceRunDiagnosticSummaryBuilderTest.php`, and `apps/platform/tests/Unit/Support/OperatorExplanation/OperatorExplanationBuilderTest.php`
|
||||||
|
- [X] T035 [P] Execute the manual smoke checks for summary-first hierarchy, zero-output runs, multi-cause runs, cross-family parity, and tenant-safe related links in `specs/220-governance-run-summaries/quickstart.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Setup tasks `T001-T003` can begin immediately.
|
||||||
|
- Foundational tasks `T004-T008` depend on setup and block all story work.
|
||||||
|
- User Story 1 depends on Phase 2 and is the MVP slice.
|
||||||
|
- User Story 2 depends on Phase 2 and the shared summary rendering established in User Story 1 because it extends the same builder and canonical detail surface.
|
||||||
|
- User Story 3 depends on Phase 2 and should follow User Story 1 because it extends the same ranking and rendering seams; it can overlap with late User Story 2 test work once the shared builder contract is stable.
|
||||||
|
- Polish tasks depend on all user stories being complete.
|
||||||
|
|
||||||
|
## Parallel Execution Examples
|
||||||
|
|
||||||
|
- **US1**: Run `T009` and `T010` together; after `T011-T012`, split `T013`, `T014`, and `T015` across different files.
|
||||||
|
- **US2**: Run `T018`, `T019`, and `T020` together; after `T021`, split `T022`, `T023`, and `T024` across resource, page, and authorization files.
|
||||||
|
- **US3**: Run `T025` and `T026` together; after `T027`, split `T028`, `T029`, and `T030` while keeping `T031` as the final authorization proof.
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
- Finish Setup and Foundational phases first so the derived summary seam and opt-in fixtures are stable.
|
||||||
|
- Deliver User Story 1 as the MVP because it provides the first operator-visible improvement on canonical run detail.
|
||||||
|
- Extend the same seam through User Story 2 to separate execution success from artifact trust across additional governance families.
|
||||||
|
- Finish with User Story 3 to lock deterministic multi-cause ranking and no-leak summary behavior.
|
||||||
|
- Close with formatting, focused proving commands, and the manual smoke pass documented in `quickstart.md`.
|
||||||
Loading…
Reference in New Issue
Block a user