export const HAL_CONFIGURATION_SCHEMA = "hal-configuration-preflight.v1"; const BUILTIN_THREADS = new Set(["servo-thread", "base-thread"]); export const VIRTUAL_HAL_USER_COMPONENTS = Object.freeze(["hal_manualtoolchange", "lineardelta", "rotarydelta"]); function text(value) { return typeof value === "string" && value.length > 0 ? value : null; } function expandHalVariables(value, variables = {}) { return value.replace(/\[([^\]]+)\]([A-Za-z_][A-Za-z0-9_.-]*)/g, (match, section, key) => { const sectionValues = variables[section] ?? variables[section.toUpperCase()] ?? {}; return Object.prototype.hasOwnProperty.call(sectionValues, key) ? String(sectionValues[key]) : match; }); } export function parseHalLine(raw, line = 0, variables = {}, { webEquivalentPrograms = [] } = {}) { if (typeof raw !== "string") return { status: "blocked", code: "hal_line_invalid", line }; const clean = expandHalVariables(raw.split("#", 1)[0].trim(), variables); if (!clean) return null; const tokens = clean.split(/\s+/); const command = tokens[0]; if (!["loadrt", "loadusr", "addf", "net", "setp", "source", "newsig", "linksp", "linkps", "linkpp", "show", "delsig", "sets", "unlinkp"].includes(command)) { return { schema: HAL_CONFIGURATION_SCHEMA, line, command, status: "blocked", code: "hal_command_unsupported" }; } if (command === "loadrt") { const module = tokens[1] ?? ""; const namesToken = tokens.find((token) => token.startsWith("names=")); return { schema: HAL_CONFIGURATION_SCHEMA, line, command, module, instances: namesToken ? namesToken.slice(6).split(",") : [], status: module.startsWith("[") ? "blocked" : "declared", code: module.startsWith("[") ? "hal_component_variable" : null }; } if (command === "loadusr") { const program = tokens.find((token, index) => index > 0 && !token.startsWith("-")) ?? ""; const virtual = VIRTUAL_HAL_USER_COMPONENTS.includes(program); const webEquivalent = webEquivalentPrograms.includes(program); return { schema: HAL_CONFIGURATION_SCHEMA, line, command, status: virtual || webEquivalent ? "checked" : "blocked", code: virtual || webEquivalent ? null : "hal_host_dynamic_loader", program, ...((virtual || webEquivalent) ? { capability: webEquivalent ? "WEB-EQUIV" : "SIM-ADAPTER" } : {}) }; } if (command === "addf") { const thread = tokens[2] ?? ""; return { schema: HAL_CONFIGURATION_SCHEMA, line, command, function: tokens[1] ?? null, thread, status: text(thread) && BUILTIN_THREADS.has(thread) ? "checked" : "blocked", code: text(thread) && BUILTIN_THREADS.has(thread) ? null : "hal_thread_undeclared" }; } return { schema: HAL_CONFIGURATION_SCHEMA, line, command, tokens: tokens.slice(1), status: "checked", code: null }; } export function preflightHalConfiguration({ lines = [], manifest = [], variables = {}, webEquivalentPrograms = [] } = {}) { const declared = new Set((Array.isArray(manifest) ? manifest : []).map((row) => row?.component).filter(Boolean)); const records = []; for (let index = 0; index < lines.length; index += 1) { const parsed = parseHalLine(lines[index], index + 1, variables, { webEquivalentPrograms }); if (!parsed) continue; if (parsed.command === "loadrt" && parsed.status === "declared" && !declared.has(parsed.module)) { parsed.status = "blocked"; parsed.code = "hal_component_undeclared"; } records.push(parsed); } const blocked = records.filter((record) => record.status === "blocked"); return { schema: HAL_CONFIGURATION_SCHEMA, status: blocked.length === 0 ? "ready" : "blocked", records, record_count: records.length, blocked_count: blocked.length, }; }