Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 4m44s
Added BaselineReadinessGate, resolution propagation, and disclosure semantics logic per Spec 385. Integrated baseline unreadiness into Customer Review Workspace and Review Packs.
281 lines
12 KiB
PHP
281 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Jobs\GenerateReviewPackJob;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\EnvironmentReviewCompletenessState;
|
|
use App\Support\ReviewPacks\ReportDisclosurePolicy;
|
|
use App\Support\ReviewPacks\ReviewPackOutputReadiness;
|
|
use App\Support\ReviewPacks\ReviewPackOutputResolutionGuidance;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
beforeEach(function (): void {
|
|
Storage::fake('exports');
|
|
});
|
|
|
|
it('maps baseline readiness blockers into publication-blocked review pack guidance', function (): void {
|
|
$readiness = spec385ReviewPackReadiness([
|
|
'state' => 'partial',
|
|
'readiness_state' => 'baseline_identity_unresolved',
|
|
'publication_blockers' => [
|
|
'Baseline subject identity must be resolved before customer-ready publication.',
|
|
],
|
|
'customer_safe_summary' => [
|
|
'readiness_state' => 'baseline_identity_unresolved',
|
|
'verified_subject_count' => 0,
|
|
'drift_subject_count' => 0,
|
|
'blocker_count' => 1,
|
|
'limitation_count' => 0,
|
|
],
|
|
]);
|
|
|
|
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
|
'review' => '/reviews/1',
|
|
'evidence' => '/evidence/1',
|
|
]);
|
|
|
|
expect($guidance['state'])->toBe(ReviewPackOutputResolutionGuidance::STATE_PUBLICATION_BLOCKED)
|
|
->and($guidance['limitations'][0]['key'])->toBe('baseline_publication_blockers_present')
|
|
->and($guidance['limitations'][0]['label'])->toBe('Baseline readiness blocked')
|
|
->and($guidance['primary_action']['label'])->toBe('Open baseline resolution')
|
|
->and($guidance['technical_details']['Baseline readiness'])->toContain('Baseline subject identity unresolved')
|
|
->and(json_encode($guidance, JSON_THROW_ON_ERROR))->not->toContain('baseline_identity_unresolved')
|
|
->and($guidance)->not->toHaveKey('provider_resource_bindings');
|
|
});
|
|
|
|
it('maps accepted baseline limitations into published-with-limitations guidance', function (): void {
|
|
$readiness = spec385ReviewPackReadiness([
|
|
'state' => 'partial',
|
|
'readiness_state' => 'baseline_compare_limited',
|
|
'publication_blockers' => [],
|
|
'limitation_codes' => ['baseline_accepted_limitations'],
|
|
'customer_safe_summary' => [
|
|
'readiness_state' => 'baseline_compare_limited',
|
|
'verified_subject_count' => 0,
|
|
'drift_subject_count' => 0,
|
|
'blocker_count' => 0,
|
|
'limitation_count' => 1,
|
|
],
|
|
]);
|
|
|
|
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
|
'review' => '/reviews/1',
|
|
]);
|
|
|
|
expect($guidance['state'])->toBe(ReviewPackOutputResolutionGuidance::STATE_PUBLISHED_WITH_LIMITATIONS)
|
|
->and(collect($guidance['limitations'])->pluck('key')->all())->toContain('baseline_accepted_limitations')
|
|
->and($guidance['primary_reason'])->toBe('Baseline limitations qualify this output.');
|
|
});
|
|
|
|
it('adds baseline readiness to customer-facing report disclosure proof', function (): void {
|
|
$readiness = spec385ReviewPackReadiness([
|
|
'state' => 'partial',
|
|
'readiness_state' => 'baseline_identity_unresolved',
|
|
'publication_blockers' => [
|
|
'Baseline subject identity must be resolved before customer-ready publication.',
|
|
],
|
|
'customer_safe_summary' => [
|
|
'readiness_state' => 'baseline_identity_unresolved',
|
|
'verified_subject_count' => 0,
|
|
'drift_subject_count' => 0,
|
|
'blocker_count' => 1,
|
|
'limitation_count' => 0,
|
|
],
|
|
]);
|
|
|
|
$policy = ReportDisclosurePolicy::evaluate([
|
|
'is_customer_facing' => true,
|
|
'audience_label' => 'Customer executive',
|
|
'show_section_appendix' => false,
|
|
'show_technical_details' => false,
|
|
], $readiness);
|
|
|
|
expect($policy['proof_states']['baseline_readiness'])->toBe(ReportDisclosurePolicy::PROOF_MISSING)
|
|
->and(collect($policy['blocking_reasons'])->pluck('key')->all())->toContain('baseline_readiness_blocked')
|
|
->and(collect($policy['mandatory_disclosures'])->pluck('key')->all())->toContain('baseline_readiness');
|
|
});
|
|
|
|
it('maps stale failed and unproven baseline proof to explicit limitation codes', function (
|
|
string $readinessState,
|
|
string $expectedCode,
|
|
string $expectedAction,
|
|
): void {
|
|
$readiness = spec385ReviewPackReadiness([
|
|
'state' => in_array($readinessState, ['baseline_compare_failed', 'baseline_compare_unproven'], true) ? 'missing' : 'stale',
|
|
'readiness_state' => $readinessState,
|
|
'publication_blockers' => [],
|
|
'customer_safe_summary' => [
|
|
'readiness_state' => $readinessState,
|
|
'verified_subject_count' => 0,
|
|
'drift_subject_count' => 0,
|
|
'blocker_count' => 0,
|
|
'limitation_count' => 0,
|
|
],
|
|
]);
|
|
|
|
$guidance = ReviewPackOutputResolutionGuidance::fromReadiness($readiness, [
|
|
'review' => '/reviews/1',
|
|
'evidence' => '/evidence/1',
|
|
'operation' => '/operations/1',
|
|
]);
|
|
|
|
expect(collect($guidance['limitations'])->pluck('key')->all())->toContain($expectedCode)
|
|
->and($guidance['primary_action']['key'])->toBe($expectedAction);
|
|
})->with([
|
|
'unproven compare' => ['baseline_compare_unproven', 'baseline_compare_unproven', 'open_evidence_basis'],
|
|
'stale compare' => ['baseline_compare_stale', 'baseline_compare_stale', 'open_evidence_basis'],
|
|
'failed compare' => ['baseline_compare_failed', 'baseline_compare_failed', 'open_operation_proof'],
|
|
]);
|
|
|
|
it('redacts baseline internal diagnostics from customer-safe review pack output but keeps them for internal output', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$review = composeEnvironmentReviewForTest($tenant, $user);
|
|
$baselineReadiness = [
|
|
'version' => 'baseline_readiness.spec385.v1',
|
|
'state' => 'partial',
|
|
'readiness_state' => 'baseline_identity_unresolved',
|
|
'publication_blockers' => [
|
|
'Baseline subject identity must be resolved before customer-ready publication.',
|
|
],
|
|
'limitations' => [
|
|
[
|
|
'code' => 'baseline_accepted_limitations',
|
|
'summary' => 'Accepted baseline limitations qualify the customer-ready claim.',
|
|
],
|
|
],
|
|
'limitation_codes' => ['baseline_accepted_limitations'],
|
|
'customer_safe_summary' => [
|
|
'readiness_state' => 'baseline_identity_unresolved',
|
|
'verified_subject_count' => 1,
|
|
'drift_subject_count' => 0,
|
|
'blocker_count' => 1,
|
|
'limitation_count' => 1,
|
|
],
|
|
'internal_diagnostics' => [
|
|
'latest_compare_run_id' => 12345,
|
|
'binding_decision_counts' => ['exact_provider_identity' => 1],
|
|
'provider_resource_id' => 'provider-policy-123',
|
|
'canonical_subject_key' => 'baseline:policy:provider-policy-123',
|
|
],
|
|
];
|
|
$review->forceFill([
|
|
'summary' => array_replace(is_array($review->summary) ? $review->summary : [], [
|
|
'baseline_readiness' => $baselineReadiness,
|
|
'baseline_publication_blockers' => [],
|
|
'baseline_limitations' => [],
|
|
'publish_blockers' => [],
|
|
]),
|
|
])->save();
|
|
|
|
$customerPack = app(ReviewPackService::class)->generateFromReview($review->fresh(), $user, [
|
|
'include_pii' => false,
|
|
'include_operations' => false,
|
|
]);
|
|
app()->call([new GenerateReviewPackJob(
|
|
reviewPackId: (int) $customerPack->getKey(),
|
|
operationRunId: (int) $customerPack->operation_run_id,
|
|
), 'handle']);
|
|
|
|
$internalPack = app(ReviewPackService::class)->generateFromReview($review->fresh(), $user, [
|
|
'include_pii' => true,
|
|
'include_operations' => false,
|
|
]);
|
|
app()->call([new GenerateReviewPackJob(
|
|
reviewPackId: (int) $internalPack->getKey(),
|
|
operationRunId: (int) $internalPack->operation_run_id,
|
|
), 'handle']);
|
|
|
|
$customerPack->refresh();
|
|
$internalPack->refresh();
|
|
|
|
$customerSummaryJson = json_encode(spec385ZipJson($customerPack, 'summary.json'), JSON_THROW_ON_ERROR);
|
|
$customerMetadataJson = json_encode(spec385ZipJson($customerPack, 'metadata.json'), JSON_THROW_ON_ERROR);
|
|
$customerExecutiveMarkdown = spec385ZipText($customerPack, ReviewPackService::EXECUTIVE_ENTRYPOINT_FILENAME);
|
|
$internalSummaryJson = json_encode(spec385ZipJson($internalPack, 'summary.json'), JSON_THROW_ON_ERROR);
|
|
|
|
expect($customerSummaryJson)->toContain('baseline_readiness')
|
|
->and($customerPack->file_path)->not->toBe($internalPack->file_path)
|
|
->and($customerSummaryJson)->toContain('Baseline subject identity unresolved', 'Published with limitations')
|
|
->and($customerSummaryJson)->not->toContain(
|
|
'internal_diagnostics',
|
|
'latest_compare_run_id',
|
|
'binding_decision_counts',
|
|
'provider_resource_id',
|
|
'canonical_subject_key',
|
|
'baseline_readiness.spec385.v1',
|
|
'baseline_identity_unresolved',
|
|
'baseline_publication_blockers_present',
|
|
'baseline_accepted_limitations',
|
|
'environment_review_id',
|
|
'snapshot_id',
|
|
'review_pack_id',
|
|
'"id":',
|
|
)
|
|
->and($customerMetadataJson)->not->toContain(
|
|
'internal_diagnostics',
|
|
'latest_compare_run_id',
|
|
'binding_decision_counts',
|
|
'provider_resource_id',
|
|
'canonical_subject_key',
|
|
'baseline_readiness.spec385.v1',
|
|
'baseline_identity_unresolved',
|
|
'baseline_publication_blockers_present',
|
|
'baseline_accepted_limitations',
|
|
'environment_review_id',
|
|
'snapshot_id',
|
|
'review_pack_id',
|
|
'"id":',
|
|
)
|
|
->and($customerExecutiveMarkdown)->toContain('current released review', 'Accepted baseline limitations qualify the customer-ready claim.')
|
|
->and($customerExecutiveMarkdown)->not->toContain('#'.$review->getKey(), '#'.$review->evidence_snapshot_id, 'baseline_identity_unresolved', 'baseline_accepted_limitations')
|
|
->and($internalSummaryJson)->toContain('internal_diagnostics', 'latest_compare_run_id', 'binding_decision_counts', 'baseline_identity_unresolved');
|
|
});
|
|
|
|
/**
|
|
* @param array<string, mixed> $baselineReadiness
|
|
* @return array<string, mixed>
|
|
*/
|
|
function spec385ReviewPackReadiness(array $baselineReadiness): array
|
|
{
|
|
return ReviewPackOutputReadiness::derive(
|
|
reviewStatus: 'published',
|
|
reviewCompletenessState: EnvironmentReviewCompletenessState::Complete->value,
|
|
evidenceCompletenessState: EnvironmentReviewCompletenessState::Complete->value,
|
|
sectionStateCounts: [EnvironmentReviewCompletenessState::Complete->value => 6],
|
|
requiredSectionCount: 6,
|
|
requiredSectionStateCounts: [EnvironmentReviewCompletenessState::Complete->value => 6],
|
|
publishBlockers: [],
|
|
hasReadyExport: true,
|
|
includePii: false,
|
|
protectedValuesHidden: true,
|
|
disclosurePresent: true,
|
|
baselineReadiness: $baselineReadiness,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
function spec385ZipJson(\App\Models\ReviewPack $pack, string $filename): array
|
|
{
|
|
$payload = json_decode(spec385ZipText($pack, $filename), true, 512, JSON_THROW_ON_ERROR);
|
|
|
|
return is_array($payload) ? $payload : [];
|
|
}
|
|
|
|
function spec385ZipText(\App\Models\ReviewPack $pack, string $filename): string
|
|
{
|
|
$zipContent = Storage::disk('exports')->get((string) $pack->file_path);
|
|
$tempFile = tempnam(sys_get_temp_dir(), 'spec385-review-pack-');
|
|
file_put_contents($tempFile, $zipContent);
|
|
|
|
$zip = new ZipArchive;
|
|
$zip->open($tempFile);
|
|
$payload = (string) $zip->getFromName($filename);
|
|
$zip->close();
|
|
unlink($tempFile);
|
|
|
|
return $payload;
|
|
}
|