<?php
declare(strict_types=1);

// Single-pass, SELECT-only developer-MySQL workload probe. Default deny.
// This does NOT invoke ThinkPHP, --check-only, HTTP, DML or a second full scan.
const PROBE_MAX_ROWS = 150000;
const PROBE_MAX_SECONDS = 25.0;
const PROBE_SCAN_SECONDS = 8.0;
const PROBE_MAX_RSS_KIB = 65536;
const PROBE_CHUNK_ROWS = 1000;
const PROBE_DEV_DB_SHA = '210f925e093f71e26a8c8923f0f626e3be0a15ccff3d386bba21feef695c8a5b';
const PROBE_REVIEWED_LOCAL_CHARSET = 'utf8';
const PROBE_ENV_FILE = '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local';
const PROBE_LOCK_FILE = '/tmp/zhct-ai-log-command-ab.lock';

$mode = (string)($argv[1] ?? '');
if (count($argv) !== 2 || !in_array($mode, ['--preflight', '--run', '--worker'], true)) {
    fwrite(STDERR, "DENIED: reviewed --preflight or --run required\n");
    exit(78);
}
$selfSha = hash_file('sha256', __FILE__);
if ($mode === '--preflight'
    && (getenv('CDX_SUBSIDY_PREFLIGHT_APPROVED') !== 'METADATA_COUNT_EXPLAIN_ONLY_V1'
        || getenv('CDX_SUBSIDY_PREFLIGHT_WINDOW') !== 'ROOT_CONFIRMED_READONLY_DEV_MYSQL'
        || !hash_equals($selfSha, (string)getenv('CDX_SUBSIDY_PREFLIGHT_SHA')))) {
    fwrite(STDERR, "DENIED: separate metadata-only preflight approval required\n");
    exit(78);
}
if ($mode === '--run'
    && (getenv('CDX_SUBSIDY_PROBE_APPROVED') !== 'SINGLE_PASS_DEV_MYSQL_READ_V1'
        || getenv('CDX_SUBSIDY_PROBE_WINDOW') !== 'ROOT_CONFIRMED_EXCLUSIVE_DEV_MYSQL'
        || !hash_equals($selfSha, (string)getenv('CDX_SUBSIDY_PROBE_SHA')))) {
    fwrite(STDERR, "DENIED: exact SHA and exclusive window approval required\n");
    exit(78);
}
if ($mode === '--worker'
    && (!hash_equals($selfSha, (string)getenv('CDX_SUBSIDY_PROBE_WORKER_SHA'))
        || (int)getenv('CDX_SUBSIDY_PROBE_PARENT_PID') !== posix_getppid())) {
    fwrite(STDERR, "DENIED: supervisor worker token required\n");
    exit(78);
}
if (PHP_VERSION_ID < 70400 || PHP_VERSION_ID >= 70500
    || realpath(PHP_BINARY) !== '/opt/homebrew/Cellar/php@7.4/7.4.33_13/bin/php'
    || !extension_loaded('pdo_mysql')) {
    fwrite(STDERR, "DENIED: approved PHP7.4 PDO MySQL required\n");
    exit(78);
}

function redactedHalt(string $stage, Throwable $error): void
{
    echo json_encode(['status' => 'halt', 'stage' => $stage,
        'class' => get_class($error), 'code' => (string)$error->getCode(),
        'error_sha256' => hash('sha256', $error->getMessage())],
        JSON_UNESCAPED_SLASHES), PHP_EOL;
    exit(3);
}

function canonicalFiveFields(array $row): string
{
    $row = array_change_key_case($row, CASE_LOWER);
    $encoded = '';
    foreach (['log_id', 'user_id', 'money', 'subsidy_balance_after',
        'operator_name'] as $name) {
        if (!array_key_exists($name, $row)) {
            throw new RuntimeException('exact five field source changed');
        }
        if ($row[$name] === null) {
            $encoded .= 'N;';
        } else {
            $value = (string)$row[$name];
            $encoded .= 'V' . strlen($value) . ':' . $value . ';';
        }
    }
    return $encoded . "\n";
}

function originalABCharsetBoundary(array $dbConfig): void
{
    $targets = [
        [13109, 18105, '/private/tmp/zhct-http-ai-macos.zjaU8h',
            'e7402b1ad50fcf4205326d93dc22e4f491c5052c60fcbe36214cd4e07b55343c'],
        [13127, 18101, '/private/tmp/zhct-http-ai-macos.x2CCZX',
            '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02'],
    ];
    foreach ($targets as [$pid, $port, $root, $configSha]) {
        $owners = trim((string)shell_exec('/usr/sbin/lsof -t -nP -iTCP:' . $port
            . ' -sTCP:LISTEN 2>/dev/null'));
        $command = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
        if ($owners !== (string)$pid
            || strpos($command, '-S 127.0.0.1:' . $port) === false
            || (strpos($command, $root . '/public') === false
                && strpos($command,
                    str_replace('/private/tmp/', '/tmp/', $root) . '/public') === false)
            || hash_file('sha256', $root . '/config/database.php') !== $configSha
            || preg_match('/(?:^|\s)POC_MYSQL_UTF8MB4=1(?=\s|$)/', $command) === 1) {
            throw new RuntimeException('A/B original runtime charset or listener drift');
        }
        foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
            'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
            'PASSWORD' => 'PASSWORD'] as $from => $to) {
            $value = (string)($dbConfig[$from] ?? '');
            if ($value === '' || preg_match('/\s/', $value)
                || preg_match('/(?:^|\s)'
                    . preg_quote('POC_AI_' . $to . '=' . $value, '/')
                    . '(?=\s|$)/', $command) !== 1) {
                throw new RuntimeException('A/B runtime differs from code-local AI connection');
            }
        }
    }
}

function runPreflight(): void
{
    // No worker, ThinkPHP command, HTTP, five-column data cursor or DML here.
    $stage = 'local-config';
    ini_set('display_errors', '0');
    ini_set('log_errors', '0');
    set_error_handler(static function (int $severity): bool {
        if (!(error_reporting() & $severity)) return false;
        throw new ErrorException('redacted warning', 0, $severity);
    });
    if (!function_exists('pcntl_alarm') || !function_exists('pcntl_async_signals')) {
        throw new RuntimeException('metadata-only preflight watchdog unavailable');
    }
    pcntl_async_signals(true);
    pcntl_signal(SIGALRM, static function (): void {
        throw new RuntimeException('metadata-only preflight 15-second deadline');
    });
    pcntl_alarm(15);
    try {
        if (realpath(PROBE_ENV_FILE) !== PROBE_ENV_FILE) {
            throw new RuntimeException('code-local config path drift');
        }
        $sections = parse_ini_file(PROBE_ENV_FILE, true, INI_SCANNER_RAW);
        $config = is_array($sections) ? ($sections['DATABASE'] ?? null) : null;
        if (!is_array($config) || strtolower((string)($config['TYPE'] ?? '')) !== 'mysql') {
            throw new RuntimeException('code-local AI MySQL config unavailable');
        }
        foreach (['HOSTNAME', 'HOSTPORT', 'DATABASE', 'USERNAME', 'PASSWORD',
            'CHARSET'] as $name) {
            if ((string)($config[$name] ?? '') === '') {
                throw new RuntimeException('code-local AI MySQL setting missing');
            }
        }
        $charset = strtolower((string)$config['CHARSET']);
        if (!preg_match('/^[a-z0-9_]+$/', $charset)
            || $charset !== PROBE_REVIEWED_LOCAL_CHARSET) {
            throw new RuntimeException('reviewed code-local CHARSET changed');
        }
        originalABCharsetBoundary($config);
        $dsn = 'mysql:host=' . $config['HOSTNAME'] . ';port=' . (int)$config['HOSTPORT']
            . ';dbname=' . $config['DATABASE'] . ';charset=' . $charset;
        $db = new PDO($dsn, $config['USERNAME'], $config['PASSWORD'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5]);
        $stage = 'approved-dev-identity';
        $identity = (string)$db->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
            ->fetchColumn();
        if (!hash_equals(PROBE_DEV_DB_SHA, hash('sha256', $identity))) {
            throw new RuntimeException('not approved development AI MySQL');
        }
        $stage = 'five-column-metadata-and-charset';
        $cursor = $db->query("SELECT COLUMN_NAME,CHARACTER_SET_NAME "
            . "FROM INFORMATION_SCHEMA.COLUMNS "
            . "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='yoshop_user_subsidy_log' "
            . "AND COLUMN_NAME IN ('log_id','user_id','money','subsidy_balance_after','operator_name')");
        $found = [];
        $operatorCharset = null;
        while (($column = $cursor->fetch(PDO::FETCH_ASSOC)) !== false) {
            $name = strtolower((string)$column['column_name']);
            $found[$name] = true;
            if ($name === 'operator_name') {
                $operatorCharset = strtolower((string)($column['character_set_name'] ?? ''));
            }
        }
        $cursor->closeCursor();
        if (count($found) !== 5 || $operatorCharset === null || $operatorCharset === '') {
            throw new RuntimeException('five target fields or source charset missing');
        }
        if ($charset === 'utf8' && $operatorCharset === 'utf8mb4') {
            echo json_encode(['status' => 'blocked-charset', 'local_charset' => $charset,
                'operator_column_charset' => $operatorCharset,
                'reason' => 'local-utf8-cannot-guarantee-four-byte-fidelity',
                'worker_started' => false, 'business_rows_fetched' => 0],
                JSON_UNESCAPED_SLASHES), PHP_EOL;
            exit(3);
        }
        $stage = 'primary-key-metadata';
        $primary = (int)$db->query("SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS "
            . "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='yoshop_user_subsidy_log' "
            . "AND INDEX_NAME='PRIMARY' AND SEQ_IN_INDEX=1 AND COLUMN_NAME='log_id'")
            ->fetchColumn();
        if ($primary !== 1) throw new RuntimeException('LOG_ID primary index unavailable');
        $stage = 'bounded-aggregate-count';
        $row = $db->query('SELECT COUNT(*) AS TOTAL_ROWS, MAX(LOG_ID) AS MAX_LOG_ID '
            . 'FROM yoshop_user_subsidy_log')->fetch(PDO::FETCH_ASSOC);
        $count = (int)($row['total_rows'] ?? -1);
        if ($count < 1 || $count > PROBE_MAX_ROWS || (int)($row['max_log_id'] ?? 0) <= 0) {
            echo json_encode(['status' => 'blocked-row-bound', 'rows' => $count,
                'max_allowed_rows' => PROBE_MAX_ROWS,
                'worker_started' => false, 'business_rows_fetched' => 0],
                JSON_UNESCAPED_SLASHES), PHP_EOL;
            exit(3);
        }
        $stage = 'explain-only-no-business-rows';
        $planCursor = $db->query('EXPLAIN SELECT LOG_ID,USER_ID,MONEY,'
            . 'SUBSIDY_BALANCE_AFTER,OPERATOR_NAME FROM yoshop_user_subsidy_log '
            . 'ORDER BY LOG_ID ASC LIMIT 150001');
        $plan = $planCursor->fetch(PDO::FETCH_ASSOC);
        $extraPlan = $planCursor->fetch(PDO::FETCH_ASSOC);
        $planCursor->closeCursor();
        if (!is_array($plan) || $extraPlan !== false) {
            throw new RuntimeException('unexpected EXPLAIN plan shape');
        }
        $key = strtoupper((string)($plan['key'] ?? ''));
        $extra = strtolower((string)($plan['extra'] ?? ''));
        $estimated = (int)($plan['rows'] ?? -1);
        $pkPlan = $key === 'PRIMARY' && $estimated >= 0
            && strpos($extra, 'filesort') === false
            && strpos($extra, 'temporary') === false;
        if (!$pkPlan) throw new RuntimeException('EXPLAIN does not promise bounded PK order');
        pcntl_alarm(0);
        echo json_encode(['status' => 'metadata-count-explain-only-pass',
            'approved_dev_identity' => true, 'five_columns' => true,
            'source_charset_compatible' => true, 'primary_log_id' => true,
            'rows' => $count, 'max_allowed_rows' => PROBE_MAX_ROWS,
            'plan_uses_primary_without_filesort' => true,
            'estimated_plan_rows' => $estimated,
            'worker_started' => false, 'business_rows_fetched' => 0,
            'think_or_http_or_dml' => false], JSON_UNESCAPED_SLASHES), PHP_EOL;
    } catch (Throwable $error) {
        pcntl_alarm(0);
        redactedHalt($stage, $error);
    }
}

function runWorker(): void
{
    ini_set('display_errors', '0');
    ini_set('log_errors', '0');
    ini_set('memory_limit', '64M');
    $stage = 'local-config';
    set_error_handler(static function (int $severity): bool {
        if (!(error_reporting() & $severity)) return false;
        throw new ErrorException('redacted warning', 0, $severity);
    });
    try {
        if (realpath(PROBE_ENV_FILE) !== PROBE_ENV_FILE) {
            throw new RuntimeException('code-local config path drift');
        }
        $sections = parse_ini_file(PROBE_ENV_FILE, true, INI_SCANNER_RAW);
        $dbConfig = is_array($sections) ? ($sections['DATABASE'] ?? null) : null;
        if (!is_array($dbConfig) || strtolower((string)($dbConfig['TYPE'] ?? '')) !== 'mysql') {
            throw new RuntimeException('code-local AI MySQL config unavailable');
        }
        foreach (['HOSTNAME', 'HOSTPORT', 'DATABASE', 'USERNAME', 'PASSWORD'] as $name) {
            if ((string)($dbConfig[$name] ?? '') === '') {
                throw new RuntimeException('code-local AI MySQL setting missing');
            }
        }
        $charset = strtolower((string)($dbConfig['CHARSET'] ?? ''));
        if (!preg_match('/^[a-z0-9_]+$/', $charset)
            || $charset !== PROBE_REVIEWED_LOCAL_CHARSET) {
            throw new RuntimeException('code-local reviewed CHARSET changed');
        }
        originalABCharsetBoundary($dbConfig);
        // Use the exact code-local charset. Never silently widen utf8 to utf8mb4.
        $dsn = 'mysql:host=' . $dbConfig['HOSTNAME'] . ';port=' . (int)$dbConfig['HOSTPORT']
            . ';dbname=' . $dbConfig['DATABASE'] . ';charset=' . $charset;
        $options = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_CASE => PDO::CASE_LOWER, PDO::ATTR_TIMEOUT => 5];
        $stage = 'approved-dev-identity';
        $meta = new PDO($dsn, $dbConfig['USERNAME'], $dbConfig['PASSWORD'], $options);
        $identity = (string)$meta->query(
            "SELECT CONCAT(@@server_uuid,'|',@@hostname,'|',@@port,'|',DATABASE())")
            ->fetchColumn();
        if (!hash_equals(PROBE_DEV_DB_SHA, hash('sha256', $identity))) {
            throw new RuntimeException('not approved development AI MySQL');
        }
        $stage = 'primary-key-and-five-columns';
        $columnCursor = $meta->query("SELECT COLUMN_NAME,CHARACTER_SET_NAME "
            . "FROM INFORMATION_SCHEMA.COLUMNS "
            . "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='yoshop_user_subsidy_log' "
            . "AND COLUMN_NAME IN ('log_id','user_id','money','subsidy_balance_after','operator_name')");
        $columns = [];
        $operatorCharset = null;
        while (($column = $columnCursor->fetch(PDO::FETCH_ASSOC)) !== false) {
            $columns[] = strtolower((string)$column['column_name']);
            if (strcasecmp((string)$column['column_name'], 'operator_name') === 0) {
                $operatorCharset = strtolower((string)($column['character_set_name'] ?? ''));
            }
        }
        $columnCursor->closeCursor();
        if (count($columns) !== 5 || $operatorCharset === null || $operatorCharset === '') {
            throw new RuntimeException('five source fields or operator charset missing');
        }
        if ($charset === 'utf8' && $operatorCharset === 'utf8mb4') {
            throw new RuntimeException('code-local utf8 cannot guarantee four-byte operator_name fidelity');
        }
        $primary = (int)$meta->query("SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS "
            . "WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='yoshop_user_subsidy_log' "
            . "AND INDEX_NAME='PRIMARY' AND SEQ_IN_INDEX=1 AND COLUMN_NAME='log_id'")
            ->fetchColumn();
        if ($primary !== 1) throw new RuntimeException('LOG_ID leading primary key absent');
        $stage = 'bounded-source-count';
        $head = $meta->query('SELECT COUNT(*) AS TOTAL_ROWS, MAX(LOG_ID) AS MAX_LOG_ID '
            . 'FROM yoshop_user_subsidy_log')->fetch(PDO::FETCH_ASSOC);
        $expected = (int)($head['total_rows'] ?? -1);
        $maxId = (int)($head['max_log_id'] ?? 0);
        if ($expected < 1 || $expected > PROBE_MAX_ROWS || $maxId <= 0) {
            throw new RuntimeException('strict 1..150000 source bound failed');
        }
        $meta = null;

        // mysqlnd's default is buffered; this cursor must remain unbuffered.
        $stage = 'single-forward-only-five-column-scan';
        $streamOptions = $options;
        $streamOptions[PDO::MYSQL_ATTR_USE_BUFFERED_QUERY] = false;
        $stream = new PDO($dsn, $dbConfig['USERNAME'], $dbConfig['PASSWORD'], $streamOptions);
        $started = microtime(true);
        $statement = $stream->query('SELECT LOG_ID,USER_ID,MONEY,SUBSIDY_BALANCE_AFTER,OPERATOR_NAME '
            . 'FROM yoshop_user_subsidy_log ORDER BY LOG_ID ASC LIMIT 150001');
        $rootDigest = hash_init('sha256');
        $chunkDigest = hash_init('sha256');
        $chunks = 0;
        $inChunk = 0;
        $count = 0;
        $lastId = 0;
        while (($row = $statement->fetch(PDO::FETCH_ASSOC)) !== false) {
            $count++;
            $logId = (int)($row['log_id'] ?? 0);
            if ($count > PROBE_MAX_ROWS || $logId <= $lastId) {
                throw new RuntimeException('row limit or LOG_ID order drift');
            }
            $lastId = $logId;
            hash_update($chunkDigest, canonicalFiveFields($row));
            $inChunk++;
            if ($inChunk === PROBE_CHUNK_ROWS) {
                hash_update($rootDigest, pack('N', ++$chunks) . hex2bin(hash_final($chunkDigest)));
                $chunkDigest = hash_init('sha256');
                $inChunk = 0;
            }
            if ($count % PROBE_CHUNK_ROWS === 0
                && (microtime(true) - $started > PROBE_SCAN_SECONDS
                    || memory_get_peak_usage(true) > PROBE_MAX_RSS_KIB * 1024)) {
                throw new RuntimeException('single scan time or PHP memory gate exceeded');
            }
        }
        if ($inChunk > 0) {
            hash_update($rootDigest, pack('N', ++$chunks) . hex2bin(hash_final($chunkDigest)));
        }
        $statement->closeCursor();
        $stream = null;
        $scanMs = (int)round((microtime(true) - $started) * 1000);
        if ($count !== $expected || $lastId !== $maxId
            || $scanMs > (int)(PROBE_SCAN_SECONDS * 1000)
            || memory_get_peak_usage(true) > PROBE_MAX_RSS_KIB * 1024) {
            throw new RuntimeException('source drift or final scan budget exceeded');
        }
        echo json_encode(['status' => 'single-pass-select-only',
            'rows' => $count, 'scan_ms' => $scanMs, 'chunks' => $chunks,
            'peak_php_mib' => round(memory_get_peak_usage(true) / 1048576, 1),
            'rolling_sha256' => hash_final($rootDigest)], JSON_UNESCAPED_SLASHES), PHP_EOL;
    } catch (Throwable $e) {
        redactedHalt($stage, $e);
    }
}

function runSupervisor(): void
{
    $lock = null;
    $process = null;
    $failure = null;
    $success = null;
    $deadline = microtime(true) + PROBE_MAX_SECONDS;
    if (!function_exists('pcntl_alarm') || !function_exists('pcntl_async_signals')) {
        throw new RuntimeException('hard wall-clock watchdog unavailable');
    }
    pcntl_async_signals(true);
    pcntl_signal(SIGALRM, static function (): void {
        throw new RuntimeException('whole probe 25 second SIGALRM deadline');
    });
    pcntl_alarm(25);
    try {
        if (is_link(PROBE_LOCK_FILE)
            || (file_exists(PROBE_LOCK_FILE) && !is_file(PROBE_LOCK_FILE))) {
            throw new RuntimeException('shared A/B log command lock path unsafe');
        }
        $lock = fopen(PROBE_LOCK_FILE, 'c');
        if (!is_resource($lock) || !flock($lock, LOCK_EX | LOCK_NB)) {
            throw new RuntimeException('shared A/B log command lock busy');
        }
        chmod(PROBE_LOCK_FILE, 0600);
        $env = ['PATH' => '/usr/bin:/bin', 'LANG' => 'C.UTF-8',
            'CDX_SUBSIDY_PROBE_WORKER_SHA' => hash_file('sha256', __FILE__),
            'CDX_SUBSIDY_PROBE_PARENT_PID' => (string)getmypid()];
        $argv = [PHP_BINARY, '-d', 'display_errors=0', '-d', 'log_errors=0',
            '-d', 'memory_limit=64M', __FILE__, '--worker'];
        $process = proc_open($argv, [0 => ['pipe', 'r'], 1 => ['pipe', 'w'],
            2 => ['pipe', 'w']], $pipes, __DIR__, $env, ['bypass_shell' => true]);
        if (!is_resource($process)) throw new RuntimeException('bounded worker could not start');
        fclose($pipes[0]);
        stream_set_blocking($pipes[1], false);
        stream_set_blocking($pipes[2], false);
        $stdout = '';
        $stderr = '';
        $rssPeakKib = 0;
        $exitCode = -1;
        while (true) {
            $stdout .= (string)stream_get_contents($pipes[1]);
            $stderr .= (string)stream_get_contents($pipes[2]);
            if (strlen($stdout) > 8192 || strlen($stderr) > 8192) {
                throw new RuntimeException('worker output exceeds 8KiB cap');
            }
            $status = proc_get_status($process);
            if (!$status['running']) {
                $exitCode = (int)$status['exitcode'];
                break;
            }
            if (microtime(true) > $deadline) {
                throw new RuntimeException('whole probe 25 second hard deadline');
            }
            $rss = trim((string)shell_exec('/bin/ps -p ' . (int)$status['pid'] . ' -o rss='));
            if ($rss === '' || !ctype_digit($rss)) {
                throw new RuntimeException('worker RSS cannot be bounded');
            }
            $rssPeakKib = max($rssPeakKib, (int)$rss);
            if ($rssPeakKib > PROBE_MAX_RSS_KIB) {
                throw new RuntimeException('worker RSS exceeds 64MiB');
            }
            usleep(100000);
        }
        $stdout .= (string)stream_get_contents($pipes[1]);
        $stderr .= (string)stream_get_contents($pipes[2]);
        if ($exitCode !== 0 || $stderr !== '' || strlen($stdout) > 8192) {
            throw new RuntimeException('worker failed or emitted stderr');
        }
        $result = json_decode(trim($stdout), true);
        if ($rssPeakKib <= 0 || !is_array($result)
            || ($result['status'] ?? '') !== 'single-pass-select-only'
            || !is_int($result['rows'] ?? null)
            || $result['rows'] > PROBE_MAX_ROWS
            || !preg_match('/^[a-f0-9]{64}$/', (string)($result['rolling_sha256'] ?? ''))) {
            throw new RuntimeException('worker bounded result contract invalid');
        }
        $success = ['status' => 'bounded-single-pass-select-only',
            'rows' => $result['rows'], 'scan_ms' => $result['scan_ms'],
            'peak_php_mib' => $result['peak_php_mib'],
            'peak_rss_mib' => round($rssPeakKib / 1024, 1),
            'rolling_sha256' => $result['rolling_sha256']];
    } catch (Throwable $e) {
        $failure = $e;
    } finally {
        pcntl_alarm(0);
        if (is_resource($process)) {
            $status = proc_get_status($process);
            if ($status['running']) proc_terminate($process, 9);
            if (isset($pipes[1]) && is_resource($pipes[1])) fclose($pipes[1]);
            if (isset($pipes[2]) && is_resource($pipes[2])) fclose($pipes[2]);
            proc_close($process);
        }
        if (is_resource($lock)) {
            flock($lock, LOCK_UN);
            fclose($lock);
        }
    }
    if ($failure !== null) redactedHalt('supervisor', $failure);
    if ($success === null) redactedHalt('supervisor', new RuntimeException('missing success result'));
    echo json_encode($success, JSON_UNESCAPED_SLASHES), PHP_EOL;
}

if ($mode === '--preflight') runPreflight();
elseif ($mode === '--worker') runWorker();
else runSupervisor();
