/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";