116 lines
5.1 KiB
PHP
116 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
/**
|
|
* Read-only acceptance check for the Grade 10 candidate intake work.
|
|
*
|
|
* Asserts (no database, no network, no file writes):
|
|
* 1. The strict intake gate PASSES for manifest.accepted.json (0 errors).
|
|
* 2. The strict intake gate still BLOCKS manifest.candidate.json, and every
|
|
* remaining blocking error belongs to a quarantined file in review-queue.json.
|
|
* 3. Every accepted entry carries an explicit canonical resource_type and a
|
|
* source reference; non-lesson files are never typed as video lessons.
|
|
* 4. Quarantined files are present on disk, absent from the accepted manifest,
|
|
* and listed in review-queue.json.
|
|
* 5. The live student manifest (backend/storage/curriculum/manifest.json) is
|
|
* unmodified, and the promotion path contains no lessons-table writes and
|
|
* no published/approved status.
|
|
*
|
|
* Usage:
|
|
* php backend/scripts/verify_grade10_accepted.php
|
|
*/
|
|
|
|
$projectRoot = dirname(__DIR__, 2);
|
|
$failures = [];
|
|
|
|
function check(bool $condition, string $message): void
|
|
{
|
|
global $failures;
|
|
echo ($condition ? "PASS " : "FAIL ") . $message . PHP_EOL;
|
|
if (!$condition) {
|
|
$failures[] = $message;
|
|
}
|
|
}
|
|
|
|
function runGate(string $projectRoot, string $manifest): array
|
|
{
|
|
$out = shell_exec(
|
|
'php ' . escapeshellarg($projectRoot . '/backend/scripts/validate_grade10_candidate.php')
|
|
. ' --json --manifest=' . escapeshellarg($manifest) . ' 2>/dev/null'
|
|
);
|
|
$decoded = json_decode((string) $out, true);
|
|
return is_array($decoded) ? $decoded : [];
|
|
}
|
|
|
|
$accepted = runGate($projectRoot, 'manifest.accepted.json');
|
|
check(($accepted['status'] ?? null) === 'passed', 'gate passes for manifest.accepted.json');
|
|
check(count($accepted['errors'] ?? [null]) === 0, 'zero blocking errors in accepted package (369 entries)');
|
|
|
|
$candidate = runGate($projectRoot, 'manifest.candidate.json');
|
|
check(($candidate['status'] ?? null) === 'blocked', 'gate still blocks the full candidate set until humans resolve quarantine');
|
|
|
|
$queue = json_decode((string) file_get_contents(
|
|
$projectRoot . '/backend/storage/curriculum/_incoming/grade_10/review-queue.json'
|
|
), true);
|
|
$quarantined = array_column($queue['quarantined'] ?? [], 'file');
|
|
sort($quarantined);
|
|
check(count($quarantined) === 3, 'review-queue lists exactly 3 quarantined files');
|
|
|
|
$stray = [];
|
|
foreach ($candidate['errors'] ?? [] as $error) {
|
|
$ctx = json_encode($error['context'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$belongs = false;
|
|
foreach ($quarantined as $q) {
|
|
if (str_contains($ctx, $q)) {
|
|
$belongs = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$belongs && ($error['code'] ?? '') !== 'qa_claim_conflict') {
|
|
$stray[] = $error;
|
|
}
|
|
}
|
|
check($stray === [], 'every remaining blocking error traces to a quarantined file');
|
|
|
|
$acceptedManifest = json_decode((string) file_get_contents(
|
|
$projectRoot . '/backend/storage/curriculum/_incoming/grade_10/manifest.accepted.json'
|
|
), true);
|
|
$acceptedFiles = array_column($acceptedManifest['lessons'] ?? [], 'file');
|
|
check(count($acceptedFiles) === 369, 'accepted manifest holds 369 entries');
|
|
check(count(array_intersect($acceptedFiles, $quarantined)) === 0, 'quarantined files are absent from the accepted manifest');
|
|
|
|
$typeViolations = 0;
|
|
foreach ($accepted['summary']['resource_types'] ?? [] as $type => $count) {
|
|
if (!in_array($type, ['lesson', 'unit_review', 'worksheet', 'lab', 'project', 'special_resource'], true)) {
|
|
$typeViolations++;
|
|
}
|
|
}
|
|
check($typeViolations === 0, 'all resource_type values use the canonical vocabulary');
|
|
check(($accepted['summary']['resource_types']['lesson'] ?? 0) === 302, '302 lesson files; reviews/labs/projects are not video lessons');
|
|
|
|
foreach ($quarantined as $q) {
|
|
check(is_file($projectRoot . '/backend/storage/curriculum/_incoming/' . $q), 'quarantined file preserved on disk: ' . $q);
|
|
}
|
|
|
|
$liveManifestStatus = shell_exec('git -C ' . escapeshellarg($projectRoot) . ' status --porcelain -- backend/storage/curriculum/manifest.json 2>/dev/null');
|
|
check(trim((string) $liveManifestStatus) === '', 'live student manifest.json untouched');
|
|
|
|
$promote = (string) file_get_contents($projectRoot . '/backend/scripts/promote_grade10_candidate.php');
|
|
check(!preg_match('/\bINTO\s+lessons\b|\bUPDATE\s+lessons\b/i', $promote), 'promotion path never writes the lessons table');
|
|
$sqlLines = array_filter(
|
|
explode("\n", $promote),
|
|
static fn (string $line): bool => (bool) preg_match('/\b(UPDATE|INSERT|DELETE|SET)\b/i', $line)
|
|
);
|
|
$sqlText = implode("\n", $sqlLines);
|
|
check(!str_contains($sqlText, 'markdown_content'), 'promotion path never writes lessons.markdown_content');
|
|
check(str_contains($promote, "'draft'"), 'promotion bundles/assets are draft-only');
|
|
check(!preg_match("/status['\"]?\s*(=>|,)\s*['\"]published['\"]/i", $promote), 'promotion path contains no published status');
|
|
check(!preg_match("/review_status.*approved|['\"]approved['\"]/i", $promote), 'promotion path contains no approval');
|
|
|
|
if ($failures !== []) {
|
|
fwrite(STDERR, count($failures) . " acceptance check(s) failed.\n");
|
|
exit(1);
|
|
}
|
|
echo "All acceptance checks passed.\n";
|