36 lines
1.5 KiB
PHP
36 lines
1.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
/** Pure watch-v1 measurement rules. This measures playback evidence, not attention. */
|
|
final class WatchIntervalPolicy
|
|
{
|
|
public static function evaluate(
|
|
string $previousType,
|
|
string $eventType,
|
|
int $previousPositionMs,
|
|
int $positionMs,
|
|
int $serverDeltaSeconds,
|
|
int $durationMs,
|
|
string $fundingSource
|
|
): array {
|
|
if ($eventType !== 'heartbeat' || !in_array($previousType, ['start', 'heartbeat', 'resume'], true)) {
|
|
return ['seconds' => 0, 'status' => 'excluded', 'reason' => 'non_contiguous_event'];
|
|
}
|
|
if ($fundingSource === 'none') {
|
|
return ['seconds' => 0, 'status' => 'excluded', 'reason' => 'unfunded_session'];
|
|
}
|
|
if ($durationMs <= 0 || $positionMs > $durationMs) {
|
|
return ['seconds' => 0, 'status' => 'review', 'reason' => 'position_out_of_bounds'];
|
|
}
|
|
$clientDeltaMs = $positionMs - $previousPositionMs;
|
|
if ($clientDeltaMs < 1000 || $clientDeltaMs > 90000 || $serverDeltaSeconds < 1 || $serverDeltaSeconds > 90) {
|
|
return ['seconds' => 0, 'status' => 'review', 'reason' => 'implausible_delta'];
|
|
}
|
|
$seconds = min((int)floor($clientDeltaMs / 1000), $serverDeltaSeconds, 60);
|
|
if ($seconds <= 0) return ['seconds' => 0, 'status' => 'excluded', 'reason' => 'zero_interval'];
|
|
return ['seconds' => $seconds, 'status' => 'measured', 'reason' => 'contiguous_heartbeat'];
|
|
}
|
|
}
|