TenantAtlas/app/Support/Verification/VerificationReportFingerprint.php
ahmido 53dc89e6ef Spec 075: Verification Checklist Framework V1.5 (fingerprint + acknowledgements) (#93)
Implements Spec 075 (V1.5) on top of Spec 074.

Highlights
- Deterministic report fingerprint (sha256) + previous_report_id linkage
- Viewer change indicator: "No changes" vs "Changed" when previous exists
- Check acknowledgements (fail|warn|block) with capability-first auth, confirmation, and audit event
- Verify-step UX polish (issues-first, primary CTA)

Testing
- Focused Pest coverage for fingerprint, previous resolver, change indicator, acknowledgements, badge semantics, DB-only viewer guard.

Notes
- Viewing remains DB-only (no external calls while rendering).

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@MacBookPro.fritz.box>
Reviewed-on: #93
2026-02-05 21:44:19 +00:00

97 lines
2.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Verification;
final class VerificationReportFingerprint
{
/**
* @param array<int, array<string, mixed>> $checks
*/
public static function forChecks(array $checks): string
{
$tuples = [];
foreach ($checks as $check) {
if (! is_array($check)) {
continue;
}
$key = self::normalizeKey($check['key'] ?? null);
$status = self::normalizeEnumString($check['status'] ?? null);
$reasonCode = self::normalizeEnumString($check['reason_code'] ?? null);
$blocking = is_bool($check['blocking'] ?? null) ? (bool) $check['blocking'] : false;
$severity = $check['severity'] ?? null;
$severity = is_string($severity) ? trim($severity) : '';
if ($severity === '') {
$severity = '';
} else {
$severity = strtolower($severity);
}
$tuples[] = [
'key' => $key,
'tuple' => implode('|', [
$key,
$status,
$blocking ? '1' : '0',
$reasonCode,
$severity,
]),
];
}
usort($tuples, static function (array $a, array $b): int {
$keyComparison = $a['key'] <=> $b['key'];
if ($keyComparison !== 0) {
return $keyComparison;
}
return $a['tuple'] <=> $b['tuple'];
});
$payload = implode("\n", array_map(static fn (array $item): string => (string) $item['tuple'], $tuples));
return hash('sha256', $payload);
}
/**
* @param array<string, mixed> $report
*/
public static function forReport(array $report): string
{
$checks = $report['checks'] ?? null;
$checks = is_array($checks) ? $checks : [];
/** @var array<int, array<string, mixed>> $checks */
return self::forChecks($checks);
}
private static function normalizeKey(mixed $value): string
{
if (! is_string($value)) {
return '';
}
$value = trim($value);
return $value === '' ? '' : $value;
}
private static function normalizeEnumString(mixed $value): string
{
if (! is_string($value)) {
return '';
}
$value = trim($value);
return $value === '' ? '' : strtolower($value);
}
}