313 lines
23 KiB
PHP
313 lines
23 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
// Independent offline review. No inference Logic, framework/bootstrap, config,
|
||
// provider, network or runtime lifecycle file is used.
|
||
require dirname(__DIR__) . '/vendor/autoload.php';
|
||
require __DIR__ . '/fixtures/CompleteTcmCase.php';
|
||
|
||
use app\adminapi\logic\tcm\PatientAiReportLogic;
|
||
use app\adminapi\service\AiRequiredSource;
|
||
use app\adminapi\service\TcmClinicalEvidenceIndex as Index;
|
||
use app\adminapi\service\TcmClinicalEvidenceContract as Contract;
|
||
use app\adminapi\service\TcmClinicalFactStateGuard as Guard;
|
||
use app\adminapi\service\TcmClinicalReportStream as Stream;
|
||
|
||
$artifacts = dirname(__DIR__, 2) . '/app/artifacts/ai_concurrency/';
|
||
$checks = 0; $failures = []; $observations = [];
|
||
$expect = static function (bool $ok, string $message) use (&$checks, &$failures): void { ++$checks; if (!$ok) $failures[] = $message; };
|
||
$snapshots = [
|
||
'TcmClinicalFactStateGuard' => 'b608f236c0b8877dbffb106ca8461596cf95ce911c060171cd20da31ca4a450c',
|
||
'TcmClinicalEvidenceContract' => 'c25e88fcc0633e957cf3a7aca4b417140b9ce9766e745c0d7b1bc2528aaaa509',
|
||
];
|
||
foreach ($snapshots as $class => $hash) {
|
||
$source = file_get_contents($artifacts . ($class === 'TcmClinicalFactStateGuard' ? 'v43r25c_before_' : 'v43r25b_before_') . $class . '.php.snapshot');
|
||
$expect(hash('sha256', $source) === $hash, 'before snapshot exact identity ' . $class);
|
||
if (hash('sha256', $source) !== $hash) throw new RuntimeException('Unverified before snapshot');
|
||
$source = preg_replace('/final class ' . $class . '\b/', 'final class IndependentBefore' . $class, $source, 1, $replaced);
|
||
$expect($replaced === 1, 'before class loaded under distinct review-only name ' . $class);
|
||
eval(substr($source, strlen('<?php')));
|
||
}
|
||
$beforeGuard = 'app\\adminapi\\service\\IndependentBeforeTcmClinicalFactStateGuard';
|
||
$beforeContract = 'app\\adminapi\\service\\IndependentBeforeTcmClinicalEvidenceContract';
|
||
$raw = (new ReflectionMethod(PatientAiReportLogic::class, 'buildSourceSnapshotFromRows'))->invoke(null, CompleteTcmCase::rows());
|
||
$submission = (new ReflectionMethod(PatientAiReportLogic::class, 'sanitizeSnapshotForUpstream'))->invoke(null, AiRequiredSource::submission($raw));
|
||
$ledger = Guard::build(CompleteTcmCase::current(), $submission);
|
||
$index = Index::build($ledger);
|
||
$sourceIdentity = hash('sha256', serialize($ledger));
|
||
$expect(serialize($ledger) === serialize($beforeGuard::build(CompleteTcmCase::current(), $submission)), 'before/current canonical source ledgers are byte identical');
|
||
$expect(hash_file('sha256', __DIR__ . '/fixtures/CompleteTcmCase.php') === 'b54c47ec11f6a99dc87bdda78629e18c8a6f37a64017e42a72db399c727cb1c6', 'immutable source fixture');
|
||
$makeHead = static function (array $inputIndex): string {
|
||
$prefix = "一、四诊\n\n";
|
||
foreach (Index::ROLES as $label => $role) {
|
||
$chosen = null;
|
||
foreach ($inputIndex['sources'] as $source) {
|
||
if ($source['scope'] !== 'current') continue;
|
||
foreach ($source['spans'] as $span) if ($span['role'] === $role && $span['recorded']) { $chosen = [$source, $span]; break 2; }
|
||
}
|
||
if ($chosen === null) {
|
||
$prefix .= $label . ($role === 'palpation' ? ":其他触诊未记录,脉象见独立脉象项。\n\n" : ":未记录\n\n");
|
||
} else {
|
||
[$source, $span] = $chosen;
|
||
$prefix .= $label . ':' . $source['date'] . '“' . trim($span['quote']) . '”(' . $source['id'] . ")。\n\n";
|
||
}
|
||
}
|
||
return $prefix . "二、八纲\n\n";
|
||
};
|
||
$head = $makeHead($index);
|
||
$expect(Guard::validate($ledger, $head)['valid'] && Contract::validate($index, $head, true)['valid'], 'independent full six-role candidate prefix is valid');
|
||
$case = static function (string $name, string $unit, bool $valid, ?array $customLedger = null, bool $streaming = true) use (
|
||
$ledger, $index, $head, $makeHead, $beforeGuard, $beforeContract, $expect, &$observations): void {
|
||
$input = $customLedger ?? $ledger;
|
||
$inputIndex = $customLedger === null ? $index : Index::build($input);
|
||
$inputHead = $customLedger === null ? $head : $makeHead($inputIndex);
|
||
$expect(Guard::validate($input, $inputHead)['valid'] && Contract::validate($inputIndex, $inputHead, true)['valid'], $name . ': fixture prefix itself is valid');
|
||
$g = Guard::validate($input, $inputHead . $unit);
|
||
$c = Contract::validate($inputIndex, $inputHead . $unit, true);
|
||
$oldG = $beforeGuard::validate($input, $inputHead . $unit);
|
||
$oldC = $beforeContract::validate($inputIndex, $inputHead . $unit, true);
|
||
$actual = $g['valid'] && $c['valid'];
|
||
$expect($actual === $valid, $name . ': composed expected ' . (int) $valid . ', got ' . json_encode([$g, $c]));
|
||
$entry = ['label' => $name, 'unit_sha256' => hash('sha256', $unit), 'expected_composed_valid' => $valid,
|
||
'before_guard_valid' => $oldG['valid'], 'before_contract_valid' => $oldC['valid'],
|
||
'before_guard_issues' => $oldG['issues'], 'before_contract_issues' => $oldC['issues'],
|
||
'after_guard_valid' => $g['valid'], 'after_contract_valid' => $c['valid'],
|
||
'after_guard_issues' => $g['issues'], 'after_contract_issues' => $c['issues']];
|
||
if ($streaming) foreach ([1, 13, 47] as $size) {
|
||
$visible = '';
|
||
$reader = new Stream($input, static function (string $delta) use (&$visible): bool { $visible .= $delta; return true; }, null, $inputHead, null, $inputIndex);
|
||
foreach (mb_str_split($unit . "\n\n", $size, 'UTF-8') as $chunk) if (!$reader->push($chunk)) break;
|
||
$done = $reader->finish();
|
||
$expect($done === $valid && $reader->rejected() === !$valid, $name . ': actual composed stream at chunk ' . $size);
|
||
$expect($valid ? $visible === $unit . "\n\n" : $visible === '', $name . ': accepted bytes exact / rejected unit unreleased');
|
||
}
|
||
$observations[] = $entry;
|
||
};
|
||
|
||
$abstracts = ['不能仅据相关阴性反推为绝对无实', '该限制不等同于证明纯虚无夹杂', '病位不在表,属里证'];
|
||
foreach ($abstracts as $number => $abstract) {
|
||
$case('abstract_allowed_' . $number, $abstract . '。', true);
|
||
foreach ([',患者无固定刺痛。', '且患者无固定刺痛。', '并患者有固定刺痛。', '患者有腰酸耳鸣。',
|
||
',脉滑。', ',舌质红绛。', '而患者无急性感染诱因。', ';本次脉细弱有力。', ',口干未记录。'] as $tailNumber => $tail)
|
||
$case('abstract_keeps_tail_' . $number . '_' . $tailNumber, $abstract . $tail, false);
|
||
}
|
||
foreach ([
|
||
'并非不能仅据相关阴性反推为绝对无实。', '不能不据相关阴性反推为绝对无实。',
|
||
'不能仅据相关阴性反推为绝对无实性皮疹。', '不能仅据相关阴性反推为绝对无实且患者无固定刺痛。',
|
||
'该限制不等同于证明纯虚无夹杂为事实。', '并非该限制不等同于证明纯虚无夹杂。',
|
||
'该限制不等同于证明纯虚无夹杂(s999)。', '该限制不等同于证明纯虚无夹杂(s0)。',
|
||
'该限制不等同于证明纯虚无夹杂(2020-05-02,s2)。',
|
||
'病位不在表,属里证,患者无腰酸耳鸣或固定刺痛。', '病位不在表,属里证,纯虚无夹杂。',
|
||
] as $number => $unit) $case('abstract_no_outer_or_clinical_exemption_' . $number, $unit, false);
|
||
$case('epistemic_annotation_existing_sources', '该限制不等同于证明纯虚无夹杂(s2、s3)。', true);
|
||
|
||
$pulse = '气虚、卫表不固证,依据为动则气短、自汗活动后加重及脉细弱无力(s1、s2、s10)。';
|
||
$case('pulse_own_positive_evidence_scope', $pulse, true);
|
||
$case('pulse_minimal_wrong_historical_source', '脉细弱无力(s14)。', false);
|
||
$case('pulse_summary_wrong_current_role_source', '卫表不固证,依据为脉细弱无力(s3)。', false);
|
||
foreach (['脉细弱有力', '脉细弱滑无力', '脉细弱无力及脉结', '脉细弱无力但脉浮', '脉不细弱无力'] as $number => $feature)
|
||
$case('pulse_no_feature_widening_' . $number, str_replace('脉细弱无力', $feature, $pulse), false);
|
||
foreach (['患者无气短,依据为脉细弱无力(s10)。', $pulse . '患者无自汗。', $pulse . '患者有固定刺痛。',
|
||
'卫表不固证,依据为脉细弱无力(2020-05-02,s10)。', '卫表不固证,依据为脉细弱无力(s14)。',
|
||
'卫表不固证,依据为患者无固定刺痛、脉细弱无力。'] as $number => $unit)
|
||
$case('pulse_scope_preserves_other_rejection_' . $number, $unit, false);
|
||
foreach (['weak_only' => '本次医师切诊:脉细弱。', 'missing' => null] as $name => $record) {
|
||
$variant = CompleteTcmCase::current(); $variant['pulse'] = $record;
|
||
$case('pulse_no_borrow_' . $name, $pulse, false, Guard::build($variant, $submission));
|
||
}
|
||
|
||
$locator = '腰肾相关资料见s2,暂不定肾虚';
|
||
$case('verified_exact_locator', $locator . '。', true);
|
||
foreach (['s0', 's02', 's3', 's10', 's14', 's999', 's2x', 's-2'] as $number => $id)
|
||
$case('locator_bad_id_' . $number, str_replace('s2', $id, $locator) . '。', false);
|
||
foreach ([',患者无腰酸耳鸣。', '且患者有腰酸耳鸣。', '并患者有固定刺痛。', '而脉滑。', '(2020-05-02,s2)。'] as $number => $tail)
|
||
$case('locator_cannot_hide_claim_' . $number, $locator . $tail, false);
|
||
$noLumbar = CompleteTcmCase::current(); $noLumbar['present_illness'] = '神疲乏力,未记录固定刺痛。';
|
||
$case('locator_actual_subject_required', $locator . '。', false, Guard::build($noLumbar, $submission));
|
||
$historicalLedger = $ledger; $historicalLedger['sources']['s2']['scope'] = 'historical';
|
||
$case('locator_same_date_historical_not_current', $locator . '。', false, $historicalLedger);
|
||
|
||
$future = '适用条件:复核症状仍以活动后乏力、气短、自汗为主,口干少量频饮及少津舌持续存在,且无需要优先处理的急性病因';
|
||
$case('future_exact_acute_prerequisite', $future . '。', true);
|
||
$case('future_without_optional_persistence', '适用条件:复核症状仍以活动后乏力为主,且无需要优先处理的急性病因。', true);
|
||
foreach ([',患者无固定刺痛。', '并无腰酸耳鸣。', '且患者有固定刺痛。', '患者无胸痛。',
|
||
',脉滑。', ',舌质红绛。', '且无急性感染诱因。', ';口干未记录。', '这已被本次排除。'] as $number => $tail)
|
||
$case('future_keeps_actual_tail_' . $number, $future . $tail, false);
|
||
foreach ([
|
||
str_replace('复核症状', '本次复核症状', $future), str_replace('且无需要', '且本次无需要', $future),
|
||
str_replace('且无需要', '且已确认无需要', $future), str_replace('复核症状仍以', '复核患者自述仍以', $future),
|
||
str_replace('口干少量频饮及少津舌', '脉滑', $future), str_replace('口干少量频饮及少津舌', '无固定刺痛', $future),
|
||
str_replace('活动后乏力、气短、自汗', '患者无固定刺痛', $future),
|
||
str_replace('活动后乏力、气短、自汗', '脉滑', $future), '并非' . $future,
|
||
$future . '(s2)',
|
||
] as $number => $unit) $case('future_no_current_or_target_leak_' . $number, $unit . '。', false);
|
||
|
||
|
||
// NEW independent r25b local-list/citation boundaries, declared before replay.
|
||
foreach ([
|
||
'脉细弱,无力(s10)。', '脉细弱,按之无力(s10)。',
|
||
'舌质淡红,少津(s9)。', '舌质淡红(s9),少津(s9)。',
|
||
'脉细弱(s10),无力(s10)。', '脉细弱无力(s10),舌质淡红(s9)。',
|
||
'舌质淡红、脉细弱无力(s9、s10)。',
|
||
] as $number => $unit) $case('r25b_valid_own_source_' . $number, $unit, true);
|
||
foreach ([
|
||
'脉细弱,无力(s14)。', '脉细弱,无力(s3)。',
|
||
'舌质淡红,少津(s3)。', '舌质淡红(s9),少津(s3)。',
|
||
'脉不浮,不滑,不紧(s14)。',
|
||
'脉细弱,无力(s14),口干(s10)。',
|
||
'脉细弱(s10),无力(s14)。',
|
||
'脉细弱,按之无力(s14)。',
|
||
'舌质淡红、脉细弱无力(s9)。', '舌质淡红、脉细弱无力(s10)。',
|
||
'脉细弱无力(s3、s14),舌质淡红(s9)。',
|
||
'舌质淡红(s9),脉细弱无力(s3、s14)。',
|
||
'本次脉细弱无力(2020-05-02,s10)。',
|
||
'脉细弱无力(s10),按之有力(s10)。',
|
||
] as $number => $unit) $case('r25b_wrong_local_source_or_state_' . $number, $unit, false);
|
||
$extraSource = $ledger;
|
||
$extraSource['sources']['s40'] = $ledger['sources']['s10'];
|
||
$extraSource['sources']['s40']['id'] = 's40';
|
||
foreach ([
|
||
'historical_scope' => ['scope' => 'historical', 'path' => 'history.0.pulse'],
|
||
'wrong_date' => ['date' => '2020-05-02'],
|
||
'opposite_state' => ['quote' => '本次医师切诊:不细、不弱,有力。'],
|
||
'unknown_record' => ['quote' => '未记录'],
|
||
'historical_clause' => ['quote' => '既往医师切诊:脉细弱无力。'],
|
||
] as $name => $delta) {
|
||
$variantLedger = $extraSource;
|
||
$variantLedger['sources']['s40'] = array_replace($variantLedger['sources']['s40'], $delta);
|
||
$case('r25b_source_cannot_launder_' . $name, '本次脉细弱无力(s40)。', false, $variantLedger);
|
||
}
|
||
$split = CompleteTcmCase::current();
|
||
$split['pulse'] = '本次医师切诊:脉细弱。';
|
||
$split['pulse_condition'] = '本次医师切诊:按之无力。';
|
||
$splitLedger = Guard::build($split);
|
||
$ids = [];
|
||
foreach ($splitLedger['sources'] as $source) if (in_array($source['field'], ['pulse', 'pulse_condition'], true)) $ids[$source['field']] = $source['id'];
|
||
$case('r25b_real_split_alias_union', '脉细弱无力(' . $ids['pulse'] . '、' . $ids['pulse_condition'] . ')。', true, $splitLedger);
|
||
$case('r25b_alias_field_not_feature_support', '脉细弱无力(' . $ids['pulse_condition'] . ')。', false, $splitLedger);
|
||
$case('r25b_cited_pulse_then_actual_force_alias', '脉细弱(' . $ids['pulse'] . '),按之无力(' . $ids['pulse_condition'] . ')。', true, $splitLedger);
|
||
$case('r25b_force_cannot_borrow_prior_cited_weak_source', '脉细弱(' . $ids['pulse'] . '),按之无力(' . $ids['pulse'] . ')。', false, $splitLedger);
|
||
foreach (['既往脉细弱无力(s10)。', '既往脉细弱无力(2020-05-02,s10)。',
|
||
'既往脉细弱无力(s10),本次脉细弱无力(s10)。'] as $number => $unit)
|
||
$case('r25b_historical_claim_cannot_cite_current_' . $number, $unit, false);
|
||
$validHistorical = $extraSource;
|
||
$validHistorical['sources']['s40'] = array_replace($validHistorical['sources']['s40'],
|
||
['scope' => 'historical', 'path' => 'history.0.pulse', 'date' => '2020-05-02', 'quote' => '2020-05-02医师切诊:脉细弱无力。']);
|
||
$case('r25b_valid_historical_own_source', '既往脉细弱无力(2020-05-02,s40)。', true, $validHistorical);
|
||
|
||
|
||
// Independent c grammar extensions. Original 1151 test remains untouched.
|
||
foreach ([
|
||
'脉细弱(s10),亦无力(s10)。', '舌质淡红(s9),亦少津(s9)。',
|
||
'脉细弱(s10),本次无力(s10)。', '舌质淡红(s9),当前少津(s9)。',
|
||
'脉细弱、舌质淡红,少津(s9、s10)。', '舌质淡红、脉细弱,无力(s9、s10)。',
|
||
] as $number => $unit) $case('r25c_supported_continuation_' . $number, $unit, true);
|
||
foreach ([
|
||
'脉细弱(s10),亦无力(s3)。', '舌质淡红(s9),亦少津(s3)。',
|
||
'脉细弱(s10),本次无力(s3)。', '舌质淡红(s9),当前少津(s3)。',
|
||
'脉细弱(s10),既往无力(s10)。',
|
||
'既往脉细弱(2020-05-02,s40),当前无力(s40)。',
|
||
'脉细弱(s10),口干(s2),脉无力(s3)。',
|
||
'舌质淡红(s9),腹部按之无力(s3),舌少津(s3)。',
|
||
'脉细弱、舌质淡红,少津(s9)。', '舌质淡红、脉细弱,无力(s10)。',
|
||
'脉细弱(s10),不无力(s10)。',
|
||
] as $number => $unit) {
|
||
$case('r25c_no_wrong_scope_role_or_polarity_' . $number, $unit, false,
|
||
str_contains($unit, 's40') ? $validHistorical : null);
|
||
}
|
||
$case('r25c_historical_current_explicit_switch', '既往脉细弱(2020-05-02,s40),本次脉细弱无力(s10)。', true, $validHistorical);
|
||
$case('r25c_historical_adjacent_bare_with_conjunction', '既往脉细弱(2020-05-02,s40),亦无力(s40)。', true, $validHistorical);
|
||
$oldLeap = CompleteTcmCase::current(); $oldLeap['diagnosis_date'] = '2020-02-29';
|
||
$oldLeapLedger = Guard::build($oldLeap);
|
||
$case('r25c_consistent_old_leap_visit', '本次脉细弱,无力(s10)。', true, $oldLeapLedger);
|
||
$parser = new ReflectionMethod(Guard::class, 'explicitExamReferencesSupported');
|
||
foreach (['脉细弱(s10),本次无力(s3)。', '舌质淡红(s9),当前少津(s3)。',
|
||
'脉细弱(s10),既往无力(s10)。', '既往脉细弱(2020-05-02,s40),当前无力(s40)。'] as $number => $text)
|
||
$expect(!$parser->invoke(null, str_contains($text, 's40') ? $validHistorical : $ledger, $text),
|
||
'r25c whole-sentence helper rejects the bounded temporal continuation that composed parsing loses ' . $number);
|
||
foreach (['脉细弱(s10),口干(s2),无力(s3)。', '脉细弱(s10),腹部按之无力(s3)。',
|
||
'舌质淡红(s9),口干(s2),少津(s3)。'] as $number => $text)
|
||
$expect($parser->invoke(null, $ledger, $text), 'r25c parser-only ordinary subject reset ' . $number);
|
||
foreach ([null, '', false, 20261023, '2026-02-29', '2026-13-01', 'not-a-date'] as $number => $date) {
|
||
$parserLedger = $extraSource;
|
||
$parserLedger['sources']['s40']['date'] = '2020-05-02';
|
||
$parserLedger['field_states']['pulse']['date'] = $date;
|
||
$expect($parser->invoke(null, $parserLedger, '本次脉细弱无力(s40)。'), 'r25c parser-only missing/invalid construction metadata retains declared trust boundary ' . $number);
|
||
}
|
||
|
||
|
||
// d-only bounded integration permutations; older suites remain untouched.
|
||
foreach ([
|
||
'脉细弱(s10), 当前亦无力(s10)。', '舌质淡红(s9), 此次亦少津(s9)。',
|
||
'脉细弱(2026-10-23,s10),本次无力(s10)。',
|
||
'脉细弱(s10),当前舌质淡红(s9),目前少津(s9)。',
|
||
'脉细弱、舌质淡红(s10、s9),当前少津(s9)。',
|
||
'舌质淡红、脉细弱(s9、s10),本次无力(s10)。',
|
||
'脉不浮(s10),当前不滑(s10)。',
|
||
] as $number => $unit) $case('r25d_correct_context_chain_' . $number, $unit, true);
|
||
foreach ([
|
||
'脉细弱(s10), 当前亦无力(s3)。', '舌质淡红(s9), 此次亦少津(s3)。',
|
||
'脉细弱(2026-10-23,s10),本次无力(s3)。',
|
||
'脉细弱(s10),当前舌质淡红(s9),目前少津(s3)。',
|
||
'脉细弱、舌质淡红(s10、s9),当前少津(s3)。',
|
||
'舌质淡红、脉细弱(s9、s10),本次无力(s3)。',
|
||
'脉不浮(s10),当前不滑(s14)。',
|
||
'脉细弱(s10),当前无力及滑(s10)。',
|
||
'脉细弱(s10),当前无力且滑(s10)。',
|
||
'脉细弱(s10),当前无力且有力(s10)。',
|
||
] as $number => $unit) $case('r25d_wrong_context_role_or_feature_' . $number, $unit, false);
|
||
$case('r25d_three_scope_switches_supported', '脉细弱(s10),既往无力(2020-05-02,s40),本次无力(s10)。', true, $validHistorical);
|
||
$case('r25d_wrong_middle_scope_not_rescued', '脉细弱(s10),既往无力(s10),本次无力(s10)。', false);
|
||
$reference = new ReflectionMethod(Guard::class, 'explicitExamReferencesSupported');
|
||
$parse = new ReflectionMethod(Guard::class, 'reportClauses');
|
||
$oldParse = new ReflectionMethod($beforeGuard, 'reportClauses');
|
||
foreach (['。', ';', ';', '!', '?', "\n", "\r\n"] as $number => $separator) {
|
||
$text = '脉细弱(s10)' . $separator . '当前无力(s3)。';
|
||
$expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'r25d independent real-boundary parser array equality ' . $number);
|
||
}
|
||
foreach (['脉细弱(s10),无力,口干,当前无力(s3)。',
|
||
'舌质淡红(s9),少津,腹部按之无力(s3),目前少津(s3)。',
|
||
'脉象资料见上文(s3),目前无力(s3)。'] as $number => $text)
|
||
$expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'r25d independent ordinary-subject reset parser equality ' . $number);
|
||
$context = [];
|
||
$expect(!$reference->invokeArgs(null, [$ledger, '脉细弱(s3)', &$context]) && $context === [], 'r25d invalid citation cannot seed checker context');
|
||
foreach ([['“','”'], ['「','」'], ['"','"']] as $number => [$open,$close]) {
|
||
$text = '切诊:2026-10-23' . $open . $ledger['sources']['s10']['quote'] . $close . '(s10),当前无力(s3)。';
|
||
$expect($parse->invoke(null, $ledger, $text) === $oldParse->invoke(null, $ledger, $text), 'r25d independent complete source quote parser array equality ' . $number);
|
||
}
|
||
|
||
// Strictly require every original file and full decoded unit; no optional skips.
|
||
$files = [
|
||
'single_openai_20261024_0_openai' => ['39056e3750c9b385f543db7da351b47657d997c5ac705e48cbd93f088b4c178e', 2, true],
|
||
'double_openai_20261024_0_openai' => ['0c03a7a8a764179e4535ea40c0c608e811598a2259ae9aefec3c3bdc3959f543', 1, true],
|
||
'double_openai_20261024_1_openai' => ['e1e5a7a26aae0f7443b00c6dc1f4db2ad0f97006fc84fd91bb0171423c21c252', 1, true],
|
||
'openai_deadline_20261024_openai_next_full90_openai' => ['084a59af11c14b06d6a9d5664db53a87617c5e0083bcd6640b52c2c8d1d7a21c', 1, true],
|
||
'single_qwen_20261024_0_qwen' => ['55ee0125d1cec5ccc344d9fef747e118d9e55b8147fd901e819d8a4a38408b8a', 2, false],
|
||
'double_qwen_20261024_0_qwen' => ['2e7cce04ca83526a17c3cda27764b2386e192df35b36ef37f3dd47f622f561ba', 2, false],
|
||
'double_qwen_20261024_1_qwen' => ['0d803a89c349f10f1317080c7232379c59b7e1f291e514b373e63960819d4b85', 2, false],
|
||
];
|
||
$retainedCount = 0; $wideningCount = 0;
|
||
foreach ($files as $stem => [$hash, $count, $valid]) {
|
||
$path = $artifacts . 'tcm_complete_v43r24_' . $stem . '.rejected.json';
|
||
if (!is_file($path)) throw new RuntimeException('Missing original evidence file');
|
||
$expect(hash_file('sha256', $path) === $hash, 'exact original rejection file ' . $stem);
|
||
$document = json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
|
||
$expect(count($document['rejections']) === $count, 'all retained units present ' . $stem);
|
||
foreach ($document['rejections'] as $number => $unit) {
|
||
++$retainedCount;
|
||
$case('original_r24_' . $stem . '_' . $number, $unit['unit'], $valid);
|
||
$g = Guard::validate($ledger, $unit['unit']);
|
||
if (in_array('NEGATION_SCOPE_WIDENED', array_column($g['issues'], 'code'), true)) ++$wideningCount;
|
||
}
|
||
$expect(hash_file('sha256', $path) === $hash, 'retained artifact unchanged ' . $stem);
|
||
}
|
||
$expect($retainedCount === 11 && $wideningCount === 5, 'exact 11 retained units and all five actual Q degree deletions preserved');
|
||
$expect(hash('sha256', serialize($ledger)) === $sourceIdentity, 'source ledger unchanged by all validation/replay paths');
|
||
$expect(!class_exists('app\\adminapi\\logic\\tcm\\DiagnosisAiLogic', false), 'inference Logic never loaded');
|
||
|
||
echo json_encode(['scope' => 'independent r25d final checker-only temporal integration review', 'assertions' => $checks,
|
||
'failures' => $failures, 'observations' => $observations, 'retained_units' => $retainedCount,
|
||
'qwen_real_degree_loss_still_rejected' => $wideningCount], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n";
|
||
exit($failures === [] ? 0 : 1);
|