<?php
declare(strict_types=1);

// Single owned category + DB-only file metadata + article. No OSS or real file.
$phase = (string)($argv[1] ?? '');
$lane = (string)($argv[2] ?? '');
$marker = (string)($argv[3] ?? '');
if (!in_array($phase, ['--probe', '--run', '--audit'], true)
    || !in_array($lane, ['B', 'C'], true)
    || !preg_match('/^CDXARTICLE[0-9A-F]{10}$/', $marker)
    || ($phase === '--run' && (string)getenv('CDX_ARTICLE_RUN')
        !== 'ONE_DB_METADATA_FIXTURE_V1')) {
    fwrite(STDERR, "DENIED: exact phase/lane/marker/run gate required\n");
    exit(78);
}
if ($phase === '--run') {
    $reviewedSha = (string)getenv('CDX_ARTICLE_REVIEWED_SHA');
    if (!preg_match('/^[a-f0-9]{64}$/', $reviewedSha)
        || ($lane === 'B' && hash_file('sha256', __FILE__) !== $reviewedSha)
        || ($lane === 'C' && (string)getenv('CDX_ARTICLE_SOURCE_CHECKED') !== '1')) {
        fwrite(STDERR, "DENIED: reviewed runner SHA gate required\n");
        exit(78);
    }
}
$stage = 'startup';
set_exception_handler(static function (Throwable $e) use (&$stage, $marker): void {
    fwrite(STDERR, 'ARTICLE_FATAL stage=' . $stage . ' marker=' . $marker
        . ' class=' . get_class($e) . "\n");
    exit(3);
});
$root = $lane === 'B'
    ? '/private/tmp/zhct-http-ai-macos.x2CCZX'
    : '/tmp/zhct-ai-http-20260929.3XDj3w';
$pid = $lane === 'B' ? 13127 : 53772;
$port = $lane === 'B' ? 18101 : 18102;
$base = 'http://127.0.0.1:' . $port;
if (!is_file($root . '/public/index.php')
    || !is_file($root . '/vendor/autoload.php')) {
    throw new RuntimeException('Isolated HTTP root absent');
}
if ($lane === 'B') {
    $stage = 'B-runtime';
    $config = parse_ini_file(
        '/Users/wiscourper/Documents/workspace/zhct/zhctproject/ai_api/.env.local',
        true, INI_SCANNER_RAW);
    if (!is_array($config) || !isset($config['DATABASE'], $config['DATABASE_ZHCT'])) {
        throw new RuntimeException('B code local config absent');
    }
    $cfg = $config['DATABASE'];
    $ps = (string)shell_exec('/bin/ps eww -p ' . $pid . ' -o command=');
    $owner = trim((string)shell_exec('/usr/sbin/lsof -nP -t -iTCP:'
        . $port . ' -sTCP:LISTEN 2>/dev/null'));
    foreach (['HOSTNAME' => 'HOST', 'HOSTPORT' => 'PORT',
        'DATABASE' => 'SCHEMA', 'USERNAME' => 'USER',
        'PASSWORD' => 'PASSWORD'] as $from => $to) {
        $expected = (string)($cfg[$from] ?? '');
        if ($expected === '' || strpos($ps, 'POC_AI_' . $to . '=' . $expected) === false) {
            throw new RuntimeException('B worker differs from code .env.local');
        }
    }
    if (strtolower((string)($cfg['TYPE'] ?? '')) !== 'mysql'
        || (string)($cfg['HOSTPORT'] ?? '') !== '3306'
        || $owner !== (string)$pid
        || strpos($ps, 'POC_DIALECT=mysql') === false
        || strpos($ps, 'POC_EXTERNAL_STUBS=1') === false
        || hash_file('sha256', $root . '/config/database.php')
            !== '8c46a6d9cd277f2950818d09162f639a62f135a5eb6553e6c61ce2c19167fd02'
        || hash_file('sha256', $root . '/config/cache.php')
            !== '98fa13484feb3db94bc4ebdd3de2f0b4ebc2c80d1cccb7a283a16f45eb9095a4'
        || hash_file('sha256', $root . '/public/index.php')
            !== '87ff41c14c0380cfba8cb99b02c8e440ad90f975e2e129dfb6e5657a36a7868d') {
        throw new RuntimeException('B isolated runtime drift');
    }
    $connect = static function () use ($cfg): PDO {
        return new PDO('mysql:host=' . $cfg['HOSTNAME']
            . ';port=' . $cfg['HOSTPORT'] . ';dbname=' . $cfg['DATABASE']
            . ';charset=utf8', $cfg['USERNAME'], $cfg['PASSWORD'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_CASE => PDO::CASE_LOWER]);
    };
    $table = ['category' => 'yoshop_article_category',
        'article' => 'yoshop_article', 'file' => 'yoshop_upload_file'];
} else {
    $stage = 'C-runtime';
    $env = [];
    foreach (explode("\0", (string)file_get_contents('/proc/' . $pid . '/environ'))
        as $item) {
        $pair = explode('=', $item, 2);
        if (count($pair) === 2) $env[$pair[0]] = $pair[1];
    }
    if (($env['POC_TARGET_ROOT'] ?? '') !== $root
        || ($env['POC_BASE_URL'] ?? '') !== $base
        || ($env['POC_DIALECT'] ?? '') !== 'dm'
        || ($env['POC_EXTERNAL_STUBS'] ?? '') !== '1'
        || ($env['POC_AI_HOST'] ?? '') !== '127.0.0.1'
        || ($env['POC_AI_PORT'] ?? '') !== '5238'
        || ($env['POC_AI_SCHEMA'] ?? '') !== 'AIZHCT_APP'
        || ($env['POC_AI_USER'] ?? '') !== 'AIZHCT_APP'
        || !in_array('dm', PDO::getAvailableDrivers(), true)) {
        throw new RuntimeException('C isolated runtime drift');
    }
    $connect = static function () use ($env): PDO {
        $d = new PDO('dm:host=127.0.0.1;port=5238;schema=AIZHCT_APP',
            $env['POC_AI_USER'], $env['POC_AI_PASSWORD'],
            [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_CASE => PDO::CASE_LOWER]);
        if ($d->query("SELECT SYS_CONTEXT('USERENV','CURRENT_SCHEMA') FROM DUAL")
            ->fetchColumn() !== 'AIZHCT_APP') {
            throw new RuntimeException('C effective schema drift');
        }
        return $d;
    };
    $table = ['category' => 'YOSHOP_ARTICLE_CATEGORY',
        'article' => 'YOSHOP_ARTICLE', 'file' => 'YOSHOP_UPLOAD_FILE'];
}
$stage = 'source-gate';
$db = $connect();
$col = static function (string $name) use ($lane): string {
    return $lane === 'C' ? '"' . $name . '"' : $name;
};
$fileColumns = implode(',', array_map($col, [
    'GROUP_ID', 'CHANNEL', 'STORAGE', 'DOMAIN', 'FILE_TYPE', 'FILE_NAME',
    'FILE_PATH', 'FILE_SIZE', 'FILE_EXT', 'COVER', 'UPLOADER_ID',
    'IS_RECYCLE', 'IS_DELETE', 'STORE_ID', 'CREATE_TIME', 'UPDATE_TIME',
]));
$insertSql = [
    'category' => 'INSERT INTO ' . $table['category']
        . ' (NAME,STATUS,SORT,STORE_ID,CREATE_TIME,UPDATE_TIME)'
        . ' VALUES (?,1,0,10001,?,?)',
    'file' => 'INSERT INTO ' . $table['file']
        . ' (' . $fileColumns . ')'
        . ' VALUES (0,10,?,?,10,?,?,0,?,?,0,0,0,10001,?,?)',
    'article' => 'INSERT INTO ' . $table['article']
        . ' (TITLE,SHOW_TYPE,CATEGORY_ID,IMAGE_ID,CONTENT,SORT,STATUS,'
        . ' VIRTUAL_VIEWS,ACTUAL_VIEWS,IS_DELETE,STORE_ID,CREATE_TIME,UPDATE_TIME)'
        . ' VALUES (?,10,?,?,?,0,1,0,0,0,10001,?,?)',
];
foreach ($insertSql as $name => $sql) { // compile-only: no DML
    try {
        $db->prepare($sql);
    } catch (PDOException $e) {
        preg_match('/\[([A-Za-z_]+)\]/', $e->getMessage(), $near);
        fwrite(STDERR, 'ARTICLE_PREPARE_GATE table=' . $name
            . ' vendor=' . (string)($e->errorInfo[1] ?? '?')
            . ' near=' . ($near[1] ?? 'unknown') . "\n");
        throw new RuntimeException('INSERT prepare gate failed before any DML');
    }
}
$rows = static function (PDO $d, string $sql, array $args = []): array {
    $s = $d->prepare($sql);
    $s->execute($args);
    return $s->fetchAll(PDO::FETCH_ASSOC);
};
$count = static function (PDO $d, string $sql, array $args = []): int {
    $s = $d->prepare($sql);
    $s->execute($args);
    return (int)$s->fetchColumn();
};
$owned = static function (PDO $d) use ($rows, $table, $marker): array {
    return [
        'category' => $rows($d, 'SELECT * FROM ' . $table['category']
            . ' WHERE STORE_ID=10001 AND NAME=?', [$marker . 'CAT']),
        'article' => $rows($d, 'SELECT * FROM ' . $table['article']
            . ' WHERE STORE_ID=10001 AND TITLE=?', [$marker . 'ARTICLE']),
        'file' => $rows($d, 'SELECT * FROM ' . $table['file']
            . ' WHERE STORE_ID=10001 AND FILE_NAME=?', [$marker . '.png']),
    ];
};
$natural = static function (PDO $d) use ($rows, $table): string {
    $category = $rows($d, 'SELECT * FROM ' . $table['category']
        . " WHERE STORE_ID=10001 AND NAME NOT LIKE 'CDXARTICLE%' ORDER BY CATEGORY_ID");
    $article = $rows($d, 'SELECT * FROM ' . $table['article']
        . " WHERE STORE_ID=10001 AND TITLE NOT LIKE 'CDXARTICLE%' ORDER BY ARTICLE_ID");
    $file = $rows($d, 'SELECT * FROM ' . $table['file']
        . " WHERE STORE_ID=10001 AND FILE_NAME NOT LIKE 'CDXARTICLE%' ORDER BY FILE_ID");
    if (count($category) > 200 || count($article) > 200 || count($file) > 30000) {
        throw new RuntimeException('Natural source snapshot cap exceeded');
    }
    return hash('sha256', json_encode([$category, $article, $file],
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
};
$original = $natural($db);
$current = $owned($db);
$ownedCounts = array_map('count', $current);
if ($phase === '--probe' || $phase === '--audit') {
    echo json_encode(['lane' => $lane, 'phase' => $phase,
        'marker' => $marker, 'owned_counts' => $ownedCounts,
        'natural_sha256' => $original,
        'business_http' => 0, 'business_dml' => 0],
        JSON_UNESCAPED_SLASHES), "\n";
    exit(0);
}
if ($ownedCounts !== ['category' => 0, 'article' => 0, 'file' => 0]) {
    throw new RuntimeException('Self marker not fresh');
}

define('TP_ENV', $lane === 'B' ? 'local' : 'poc');
define('BUSINESS', 'aizhct');
define('ENTERPRISE_NO', '10001');
require_once $root . '/vendor/autoload.php';
(new \think\App($root))->setEnvName($lane === 'B' ? 'local' : 'poc')->initialize();
if (config('cache.default') !== 'file') {
    throw new RuntimeException('Isolated File cache not effective');
}
$boundaryRaw = @file_get_contents($base . '/__poc_boundary', false,
    stream_context_create(['http' => ['timeout' => 5]]));
$boundary = is_string($boundaryRaw) ? json_decode($boundaryRaw, true) : null;
if ($lane === 'C' && (!is_array($boundary) || empty($boundary['external_stubs']))) {
    throw new RuntimeException('C external stub boundary absent');
}
if ($lane === 'B' && !is_string($boundaryRaw)) {
    throw new RuntimeException('B HTTP boundary absent');
}

$journalDir = sys_get_temp_dir() . '/cdx-article-' . strtolower($lane)
    . '-' . strtolower(bin2hex(random_bytes(6)));
if (!mkdir($journalDir, 0700) || !chmod($journalDir, 0700)) {
    throw new RuntimeException('Private recovery journal directory unavailable');
}
$journalPath = $journalDir . '/journal.json';
$journal = ['lane' => $lane, 'marker' => $marker,
    'phase' => 'before-fixture', 'ids' => [], 'created_at' => time(),
    'natural_sha256' => $original];
$saveJournal = static function () use (&$journal, $journalPath): void {
    $encoded = json_encode($journal, JSON_UNESCAPED_SLASHES);
    $tmp = $journalPath . '.tmp';
    if (!is_string($encoded) || file_put_contents($tmp, $encoded, LOCK_EX) !== strlen($encoded)
        || !chmod($tmp, 0600) || !rename($tmp, $journalPath)) {
        throw new RuntimeException('Private recovery journal write failed');
    }
};
$saveJournal();
echo json_encode(['status' => 'recovery_journal_ready', 'lane' => $lane,
    'marker' => $marker, 'journal' => $journalPath,
    'business_http' => 0, 'business_dml' => 0]), "\n";
flush();

$ids = ['category' => 0, 'article' => 0, 'file' => 0];
$token = 'CDXARTICLETOKEN' . strtoupper(bin2hex(random_bytes(16)));
$tokenIssued = false;
$unsettled = false;
$fixtureCommitted = false;
$out = ['lane' => $lane, 'marker' => $marker,
    'natural_sha256' => $original, 'journal' => $journalPath,
    'ids' => $ids, 'routes' => [],
    'token_deleted' => false, 'cleanup_zero' => false,
    'no_real_file_or_oss' => true];
$fixture = static function () use ($connect, $table, $rows, $marker, $owned,
    $insertSql,
    $natural, $original, &$ids, &$journal, $saveJournal): void {
    $d = $connect();
    $now = time();
    $d->beginTransaction();
    try {
        $s = $d->prepare($insertSql['category']);
        $s->execute([$marker . 'CAT', $now, $now]);
        $r = $rows($d, 'SELECT CATEGORY_ID FROM ' . $table['category']
            . ' WHERE STORE_ID=10001 AND NAME=?', [$marker . 'CAT']);
        if (count($r) !== 1) throw new RuntimeException('Category identity unresolved');
        $ids['category'] = (int)$r[0]['category_id'];

        $s = $d->prepare($insertSql['file']);
        $s->execute(['local', '', $marker . '.png',
            'codex/' . strtolower($marker) . '.png', 'png', '', $now, $now]);
        $r = $rows($d, 'SELECT FILE_ID FROM ' . $table['file']
            . ' WHERE STORE_ID=10001 AND FILE_NAME=?', [$marker . '.png']);
        if (count($r) !== 1) throw new RuntimeException('File metadata identity unresolved');
        $ids['file'] = (int)$r[0]['file_id'];

        $content = '<p>' . $marker . ' 中文测试</p>';
        $s = $d->prepare($insertSql['article']);
        $s->execute([$marker . 'ARTICLE', $ids['category'], $ids['file'],
            $content, $now, $now]);
        $r = $rows($d, 'SELECT ARTICLE_ID FROM ' . $table['article']
            . ' WHERE STORE_ID=10001 AND TITLE=?', [$marker . 'ARTICLE']);
        if (count($r) !== 1) throw new RuntimeException('Article identity unresolved');
        $ids['article'] = (int)$r[0]['article_id'];
        if (min($ids) <= 0) throw new RuntimeException('Fixture identity invalid');
        $own = $owned($d);
        if (array_map('count', $own) !== ['category' => 1, 'article' => 1, 'file' => 1]
            || (int)$own['article'][0]['category_id'] !== $ids['category']
            || (int)$own['article'][0]['image_id'] !== $ids['file']
            || (string)$own['file'][0]['storage'] !== 'local'
            || $natural($d) !== $original) {
            throw new RuntimeException('Fixture link or natural source gate failed');
        }
        $d->commit();
        $journal['phase'] = 'fixture-committed';
        $journal['ids'] = $ids;
        $saveJournal();
    } catch (Throwable $e) {
        if ($d->inTransaction()) $d->rollBack();
        throw $e;
    }
};

$shape = null;
$shape = static function ($v) use (&$shape) {
    if (is_array($v)) {
        if (array_values($v) === $v) {
            return ['@array' => count($v), '@item' => count($v) ? $shape($v[0]) : null];
        }
        $r = [];
        foreach ($v as $k => $item) $r[(string)$k] = $shape($item);
        ksort($r);
        return $r;
    }
    if (is_int($v)) return 'integer';
    if (is_float($v)) return 'double';
    if (is_bool($v)) return 'boolean';
    if (is_string($v)) return 'string';
    return 'null';
};
$request = static function (string $path, array $query, ?string $token) use ($base): array {
    $url = $base . '/index.php?s=/store/' . $path;
    if ($query) $url .= '&' . http_build_query($query);
    $header = $token === null ? '' : 'Access-Token: ' . $token . "\r\n";
    $context = stream_context_create(['http' => [
        'method' => 'GET', 'header' => $header, 'timeout' => 15,
        'ignore_errors' => true, 'follow_location' => 0,
    ]]);
    $start = microtime(true);
    $raw = @file_get_contents($url, false, $context);
    preg_match('/HTTP\/\S+\s+(\d+)/', $http_response_header[0] ?? '', $m);
    $json = is_string($raw) ? json_decode($raw, true) : null;
    unset($raw);
    return ['http' => isset($m[1]) ? (int)$m[1] : 0,
        'json' => is_array($json) ? $json : null,
        'elapsed_ms' => (int)round((microtime(true) - $start) * 1000)];
};
$unexpectedRefs = static function (PDO $d, array $actual) use ($lane, $rows,
    $count, $table): array {
    $otherArticles = $count($d, 'SELECT COUNT(*) FROM ' . $table['article']
        . ' WHERE CATEGORY_ID=? AND ARTICLE_ID<>?',
        [$actual['category'], $actual['article']]);
    $metaSql = $lane === 'B'
        ? "SELECT TABLE_NAME,COLUMN_NAME FROM information_schema.COLUMNS"
            . " WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME LIKE 'yoshop_%'"
            . " AND COLUMN_NAME IN ('image_id','file_id')"
        : "SELECT TABLE_NAME,COLUMN_NAME FROM USER_TAB_COLUMNS"
            . " WHERE TABLE_NAME LIKE 'YOSHOP_%'"
            . " AND COLUMN_NAME IN ('IMAGE_ID','FILE_ID')";
    $foreignFiles = 0;
    $examined = 0;
    foreach ($rows($d, $metaSql) as $ref) {
        $name = (string)$ref['table_name'];
        $column = (string)$ref['column_name'];
        if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)
            || !in_array(strtolower($column), ['image_id', 'file_id'], true)) {
            throw new RuntimeException('Reference metadata invalid');
        }
        if (strcasecmp($name, $table['file']) === 0 && strcasecmp($column, 'FILE_ID') === 0) {
            continue; // Own upload_file primary key, not a referencing row.
        }
        $sql = 'SELECT COUNT(*) FROM ' . $name . ' WHERE ' . $column . '=?';
        $args = [$actual['file']];
        if (strcasecmp($name, $table['article']) === 0
            && strcasecmp($column, 'IMAGE_ID') === 0) {
            $sql .= ' AND ARTICLE_ID<>?';
            $args[] = $actual['article'];
        }
        $foreignFiles += $count($d, $sql, $args);
        $examined++;
        if ($examined > 50) throw new RuntimeException('Reference table scan cap exceeded');
    }
    return ['other_articles_using_category' => $otherArticles,
        'other_rows_using_file_id' => $foreignFiles,
        'reference_columns_examined' => $examined];
};
$cleanup = static function () use ($connect, $rows, $table, $owned,
    $marker, &$ids, $natural, $original, $unexpectedRefs): array {
    $d = $connect();
    $self = $owned($d);
    $counts = array_map('count', $self);
    if ($counts === ['category' => 0, 'article' => 0, 'file' => 0]) {
        return ['zero' => true, 'natural_unchanged' => $natural($d) === $original];
    }
    if ($counts !== ['category' => 1, 'article' => 1, 'file' => 1]) {
        throw new RuntimeException('Ambiguous owned marker cleanup refused');
    }
    $actual = [
        'category' => (int)$self['category'][0]['category_id'],
        'article' => (int)$self['article'][0]['article_id'],
        'file' => (int)$self['file'][0]['file_id'],
    ];
    if (min($actual) <= 0
        || ($ids['category'] > 0 && $ids['category'] !== $actual['category'])
        || ($ids['article'] > 0 && $ids['article'] !== $actual['article'])
        || ($ids['file'] > 0 && $ids['file'] !== $actual['file'])
        || (int)$self['article'][0]['category_id'] !== $actual['category']
        || (int)$self['article'][0]['image_id'] !== $actual['file']) {
        throw new RuntimeException('Owned marker link/ID cleanup refused');
    }
    $ids = $actual;
    $refs = $unexpectedRefs($d, $actual);
    if ($refs['other_articles_using_category'] !== 0
        || $refs['other_rows_using_file_id'] !== 0) {
        throw new RuntimeException('Unexpected external row reference; cleanup refused');
    }
    $d->beginTransaction();
    try {
        foreach ([
            ['article', 'ARTICLE_ID', 'TITLE', $marker . 'ARTICLE'],
            ['file', 'FILE_ID', 'FILE_NAME', $marker . '.png'],
            ['category', 'CATEGORY_ID', 'NAME', $marker . 'CAT'],
        ] as [$name, $pk, $field, $value]) {
            $s = $d->prepare('DELETE FROM ' . $table[$name]
                . ' WHERE ' . $pk . '=? AND STORE_ID=10001 AND ' . $field . '=?');
            $s->execute([$actual[$name], $value]);
            if ($s->rowCount() !== 1) {
                throw new RuntimeException('Exact cleanup affected-row mismatch');
            }
        }
        $d->commit();
    } catch (Throwable $e) {
        if ($d->inTransaction()) $d->rollBack();
        throw $e;
    }
    $verify = $connect();
    $zero = array_map('count', $owned($verify))
        === ['category' => 0, 'article' => 0, 'file' => 0];
    return ['zero' => $zero, 'natural_unchanged' => $natural($verify) === $original,
        'ids' => $actual, 'reference_scan' => $refs];
};

try {
    $stage = 'fixture-insert';
    $journal['phase'] = $stage;
    $saveJournal();
    $fixture();
    $fixtureCommitted = true;
    $out['ids'] = $ids;
    $out['fixture_cross_links'] = true;
    $fixtureSha = hash('sha256', json_encode($owned($connect()),
        JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
    $out['fixture_sha256'] = $fixtureSha;

    $stage = 'token-issue';
    if (!\think\facade\Cache::set($token, ['user' => [
            'store_user_id' => 2147480004, 'store_id' => 10001,
            'user_name' => $marker, 'real_name' => $marker, 'is_super' => 1,
        ], 'store_id' => 10001, 'is_login' => true], 300)
        || !\think\facade\Cache::has($token)) {
        throw new RuntimeException('Isolated File token unavailable');
    }
    $tokenIssued = true;
    $unauth = $request('content.article.category/list', [], null);
    $out['unauth_category'] = ['http' => $unauth['http'],
        'status' => $unauth['json']['status'] ?? null,
        'rejected' => is_array($unauth['json'])
            && (int)($unauth['json']['status'] ?? 200) !== 200];
    if (!$out['unauth_category']['rejected']) {
        throw new RuntimeException('Unauthenticated category not rejected');
    }

    foreach ([
        'category_list' => ['content.article.category/list', []],
        'article_list' => ['content.article/list', ['title' => $marker]],
        'article_detail' => ['content.article/detail', ['articleId' => $ids['article']]],
    ] as $name => [$path, $query]) {
        $stage = $name;
        $journal['phase'] = 'request-started-' . $name;
        $saveJournal();
        $response = $request($path, $query, $token);
        if ($response['http'] === 0 || !is_array($response['json'])) {
            $unsettled = true;
            throw new RuntimeException('HTTP transport/JSON completion unknown');
        }
        $json = $response['json'];
        $data = $json['data'] ?? null;
        if ($name === 'category_list') {
            $list = $data['list'] ?? null;
            $pk = 'category_id';
            $want = $ids['category'];
        } elseif ($name === 'article_list') {
            $list = $data['list']['data'] ?? null;
            $pk = 'article_id';
            $want = $ids['article'];
        } else {
            $list = isset($data['detail']) ? [$data['detail']] : null;
            $pk = 'article_id';
            $want = $ids['article'];
        }
        $ownItems = is_array($list) ? array_values(array_filter($list,
            static function ($item) use ($pk, $want): bool {
                return is_array($item) && (int)($item[$pk] ?? 0) === $want;
            })) : [];
        $check = $connect();
        $self = $owned($check);
        $counts = array_map('count', $self);
        $ownedUnchanged = hash('sha256', json_encode($self,
            JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) === $fixtureSha;
        $ownRowMatches = count($ownItems) === 1;
        if ($name !== 'category_list' && is_array($list)) {
            $ownRowMatches = $ownRowMatches && count($list) === 1;
        }
        $noMutation = $counts === ['category' => 1, 'article' => 1, 'file' => 1]
            && $ownedUnchanged && $natural($check) === $original;
        $out['routes'][$name] = [
            'http' => $response['http'], 'status' => $json['status'] ?? null,
            'elapsed_ms' => $response['elapsed_ms'],
            'returned_count' => is_array($list) ? count($list) : null,
            'own_id_found_once' => $ownRowMatches,
            'fixture_and_natural_unchanged' => $noMutation,
            'owned_sha_unchanged' => $ownedUnchanged,
            'json_type_shape' => $shape($json),
            'owned_item_type_shape' => $ownRowMatches ? $shape($ownItems[0]) : null,
        ];
        if ($response['http'] !== 200 || (int)($json['status'] ?? -1) !== 200
            || !$ownRowMatches || !$noMutation) {
            throw new RuntimeException('Article GET/source contract mismatch');
        }
        $journal['phase'] = 'request-completed-' . $name;
        $saveJournal();
    }
} catch (Throwable $e) {
    $out['failure_stage'] = $stage;
    $out['failure_class'] = get_class($e);
} finally {
    if ($tokenIssued) {
        \think\facade\Cache::delete($token);
        $out['token_deleted'] = !\think\facade\Cache::has($token);
    }
    if ($unsettled) {
        $journal['phase'] = 'request-unsettled-manual-audit';
        $journal['ids'] = $ids;
        $saveJournal();
        $out['cleanup_deferred'] = true;
    } else {
        try {
            $stage = 'cleanup';
            $journal['phase'] = 'cleanup-started';
            $journal['ids'] = $ids;
            $saveJournal();
            $clean = $cleanup();
            $out['cleanup_zero'] = $clean['zero'];
            $out['final_natural_unchanged'] = $clean['natural_unchanged'];
            $out['cleanup_reference_scan'] = $clean['reference_scan'] ?? null;
            $journal['phase'] = $clean['zero'] ? 'cleaned-zero' : 'cleanup-not-zero';
            $journal['ids'] = $ids;
            $saveJournal();
        } catch (Throwable $e) {
            $out['cleanup_failure_class'] = get_class($e);
            $journal['phase'] = 'cleanup-failed-manual-audit';
            $journal['ids'] = $ids;
            $saveJournal();
        }
    }
}
$out['pass'] = !isset($out['failure_class'])
    && !isset($out['cleanup_failure_class'])
    && !$unsettled && $fixtureCommitted
    && count($out['routes']) === 3
    && $out['token_deleted'] && $out['cleanup_zero']
    && ($out['final_natural_unchanged'] ?? false);
echo json_encode($out, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), "\n";
exit($out['pass'] ? 0 : 3);
