1import { solveLevel } from './solve-level.js';23export const DEFAULT_SOLVER_BUDGET = 2_000_000;45/**6 * Run the authoring checks that a bundled-world gate needs without parsing CLI7 * output. HANDS-OFF is reported as a warning because a level may intentionally8 * be solvable by moving no words at all; BYPASS is a hard authoring contract.9 */10export function checkLevel(level, {11 cap = level.solverBudget ?? DEFAULT_SOLVER_BUDGET,12 maxVisited = Infinity,13 strictBypass = true,14} = {}) {15 const result = solveLevel(level, { cap, maxVisited, transitionMode: 'fast' });16 const checks = { result, bypass: null, handsOff: null, mustForm: [] };17 const issues = [];18 const warnings = [];1920 if (result.status !== 'SOLVED') issues.push(`${result.status} after ${result.explored} states`);21 if (result.status === 'SOLVED') {22 checks.bypass = result.routeBypass23 ? result24 : solveLevel(level, { cap, maxVisited, transitionMode: 'fast', wordMotion: 'player' });25 checks.handsOff = result.routeHandsOff26 ? result27 : checks.bypass.status === 'SOLVED' && checks.bypass.routeHandsOff28 ? checks.bypass29 : checks.bypass.status === 'UNSOLVED'30 ? checks.bypass31 : solveLevel(level, { cap, maxVisited, transitionMode: 'fast', wordMotion: 'all' });3233 if (checks.bypass.status === 'LIMIT') {34 issues.push(`BYPASS search LIMIT after ${checks.bypass.explored} states`);35 } else if (checks.bypass.status === 'SOLVED' && !level.allowBypass) {36 const message = 'BYPASS route exists but allowBypass is not enabled';37 (strictBypass ? issues : warnings).push(message);38 }39 if (checks.handsOff.status === 'LIMIT') {40 warnings.push(`HANDS-OFF search LIMIT after ${checks.handsOff.explored} states`);41 } else if (checks.handsOff.status === 'SOLVED' && !level.allowHandsOff) {42 warnings.push('HANDS-OFF route exists but allowHandsOff is not enabled');43 }44 }4546 // A failed normal proof already blocks the pack; avoid spending its budget47 // again on necessity diagnostics that cannot change the gate result.48 if (result.status !== 'SOLVED') return { ok: issues.length === 0, cap, checks, issues, warnings };49 for (const rule of level.mustForm || []) {50 const check = solveLevel(level, { cap, maxVisited, forbid: rule, transitionMode: 'fast' });51 checks.mustForm.push({ rule, result: check });52 if (check.status === 'SOLVED') issues.push(`SKIPPABLE(${rule})`);53 else if (check.status === 'LIMIT') issues.push(`LIMIT(${rule})`);54 }5556 return { ok: issues.length === 0, cap, checks, issues, warnings };57}5859export default checkLevel;