Implement configuration, runtime, frontend, and task governance closure

This commit is contained in:
2026-08-21 16:51:42 +08:00
parent b1b5a3cc8a
commit 63c0408ce7
92 changed files with 343619 additions and 172 deletions

View File

@@ -0,0 +1,145 @@
const CONFIGURATION_SCHEMA = "configuration-startup-closure.v1";
export const CONFIGURATION_ERRORS = Object.freeze({
invalid_reference: "configuration_reference_invalid",
empty_reference: "configuration_reference_empty",
host_path: "configuration_host_path",
outside_repo: "configuration_outside_repo",
path_escape: "configuration_path_escape",
missing: "configuration_resource_missing",
ambiguous: "configuration_resource_ambiguous",
unavailable: "configuration_resource_unavailable",
});
export const LIBRARY_ROOTS = Object.freeze([
"linuxcnc/lib/hallib",
"linuxcnc/lib",
]);
function asPathSet(paths) {
if (paths instanceof Set) return paths;
if (Array.isArray(paths)) return new Set(paths);
if (paths && typeof paths === "object") return new Set(Object.keys(paths));
return new Set();
}
function normalizePath(value) {
if (typeof value !== "string" || value.length === 0) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
}
if (value.includes("\0")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
}
const parts = [];
for (const part of value.replaceAll("\\", "/").split("/")) {
if (!part || part === ".") continue;
if (part === "..") {
if (parts.length === 0) return { status: "blocked", code: CONFIGURATION_ERRORS.path_escape, path: null };
parts.pop();
continue;
}
parts.push(part);
}
return { status: "accepted", code: null, path: parts.join("/") };
}
function dirname(path) {
const index = path.lastIndexOf("/");
return index < 0 ? "" : path.slice(0, index);
}
function joinPath(base, value) {
return normalizePath(base ? `${base}/${value}` : value);
}
export function splitReference(rawValue) {
if (typeof rawValue !== "string") {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: null, args: [] };
}
const raw = rawValue.trim().replace(/^['"]|['"]$/g, "");
if (!raw) return { status: "blocked", code: CONFIGURATION_ERRORS.empty_reference, value: null, args: [] };
const tokens = raw.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g) ?? [];
if (tokens.length === 0) return { status: "blocked", code: CONFIGURATION_ERRORS.empty_reference, value: null, args: [] };
const value = tokens[0].replace(/^['"]|['"]$/g, "");
const args = tokens.slice(1).map((token) => token.replace(/^['"]|['"]$/g, ""));
return { status: "accepted", code: null, raw, value, args };
}
export function resolveReference({ rawValue, entrypoint, availablePaths = [], libraryRoots = LIBRARY_ROOTS } = {}) {
const parsed = splitReference(rawValue);
if (parsed.status !== "accepted") return { ...parsed, path: null, attempted_paths: [] };
if (typeof entrypoint !== "string" || entrypoint.length === 0) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
if (parsed.value.startsWith("~") || parsed.value.startsWith("$") || parsed.value.startsWith("${")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.host_path, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
const available = asPathSet(availablePaths);
const entry = normalizePath(entrypoint);
if (entry.status !== "accepted") return { ...entry, value: parsed.value, args: parsed.args, attempted_paths: [] };
let candidates;
let resolution = "relative";
if (parsed.value.startsWith("LIB:")) {
resolution = "library";
const libraryName = parsed.value.slice("LIB:".length);
if (!libraryName || libraryName.startsWith("/") || libraryName.includes("..")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
}
candidates = libraryRoots.map((root) => normalizePath(`${root}/${libraryName}`)).filter((item) => item.status === "accepted").map((item) => item.path);
} else if (parsed.value.startsWith("/")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.outside_repo, value: parsed.value, args: parsed.args, path: null, attempted_paths: [] };
} else if (parsed.value.startsWith("linuxcnc/")) {
resolution = "repo_relative";
const normalized = normalizePath(parsed.value);
if (normalized.status !== "accepted") return { ...normalized, value: parsed.value, args: parsed.args, attempted_paths: [] };
candidates = [normalized.path];
} else {
const normalized = joinPath(dirname(entry.path), parsed.value);
if (normalized.status !== "accepted") return { ...normalized, value: parsed.value, args: parsed.args, attempted_paths: [] };
if (normalized.path !== "linuxcnc" && !normalized.path.startsWith("linuxcnc/")) {
return { status: "blocked", code: CONFIGURATION_ERRORS.outside_repo, value: parsed.value, args: parsed.args, path: null, attempted_paths: [normalized.path], resolution };
}
candidates = [normalized.path];
}
const existing = candidates.filter((candidate) => available.has(candidate));
if (existing.length > 1) {
return { status: "blocked", code: CONFIGURATION_ERRORS.ambiguous, value: parsed.value, args: parsed.args, path: null, attempted_paths: candidates, resolution };
}
if (existing.length === 1) {
return { status: "resolved", code: null, value: parsed.value, args: parsed.args, path: existing[0], attempted_paths: candidates, resolution };
}
return { status: "blocked", code: CONFIGURATION_ERRORS.missing, value: parsed.value, args: parsed.args, path: null, attempted_paths: candidates, resolution };
}
export function resolveResourceRow(row, availablePaths, options = {}) {
if (!row || typeof row !== "object") return { status: "blocked", code: CONFIGURATION_ERRORS.invalid_reference, path: null };
const result = resolveReference({
rawValue: row.raw_value ?? row.path,
entrypoint: row.entrypoint,
availablePaths,
...options,
});
return {
resource_source_id: row.resource_source_id ?? null,
entrypoint: row.entrypoint ?? null,
original_path: row.path ?? null,
original_status: row.parse_status ?? null,
...result,
};
}
export function preflightConfiguration({ entrypoint, rows = [], availablePaths = [], libraryRoots = LIBRARY_ROOTS } = {}) {
const resolved = rows.map((row) => resolveResourceRow(row, availablePaths, { libraryRoots }));
const blocked = resolved.filter((row) => row.status !== "resolved");
return {
schema: CONFIGURATION_SCHEMA,
entrypoint,
status: blocked.length === 0 ? "ready" : "blocked",
resource_count: resolved.length,
resolved_count: resolved.length - blocked.length,
blocked_count: blocked.length,
resources: resolved,
};
}
export { CONFIGURATION_SCHEMA };

View File

@@ -0,0 +1,49 @@
export const HAL_CONFIGURATION_SCHEMA = "hal-configuration-preflight.v1";
const BUILTIN_THREADS = new Set(["servo-thread", "base-thread"]);
function text(value) {
return typeof value === "string" && value.length > 0 ? value : null;
}
export function parseHalLine(raw, line = 0) {
if (typeof raw !== "string") return { status: "blocked", code: "hal_line_invalid", line };
const clean = raw.split("#", 1)[0].trim();
if (!clean) return null;
const tokens = clean.split(/\s+/);
const command = tokens[0];
if (!["loadrt", "loadusr", "addf", "net", "setp", "source", "newsig", "linksp", "linkps", "linkpp"].includes(command)) return null;
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") return { schema: HAL_CONFIGURATION_SCHEMA, line, command, status: "blocked", code: "hal_host_dynamic_loader", program: tokens[1] ?? "" };
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 = [] } = {}) {
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);
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,
};
}

View File

@@ -0,0 +1,34 @@
import assert from "node:assert/strict";
import { preflightConfiguration, resolveReference, splitReference } from "./configuration.mjs";
assert.deepEqual(splitReference("LIB:basic_sim.tcl -no_sim_spindle"), {
status: "accepted",
code: null,
raw: "LIB:basic_sim.tcl -no_sim_spindle",
value: "LIB:basic_sim.tcl",
args: ["-no_sim_spindle"],
});
const available = new Set([
"linuxcnc/lib/hallib/basic_sim.tcl",
"linuxcnc/configs/sim/demo/core.hal",
"linuxcnc/nc_files/probe.ngc",
]);
assert.equal(resolveReference({ rawValue: "LIB:basic_sim.tcl -no_sim_spindle", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).path, "linuxcnc/lib/hallib/basic_sim.tcl");
assert.equal(resolveReference({ rawValue: "core.hal", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).path, "linuxcnc/configs/sim/demo/core.hal");
assert.equal(resolveReference({ rawValue: "linuxcnc/nc_files/probe.ngc", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).path, "linuxcnc/nc_files/probe.ngc");
assert.equal(resolveReference({ rawValue: "~/linuxcnc/nc_files", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).code, "configuration_host_path");
assert.equal(resolveReference({ rawValue: "../../../../etc/passwd", entrypoint: "linuxcnc/configs/sim/demo/main.ini", availablePaths: available }).code, "configuration_outside_repo");
const report = preflightConfiguration({
entrypoint: "linuxcnc/configs/sim/demo/main.ini",
availablePaths: available,
rows: [
{ resource_source_id: "r1", entrypoint: "linuxcnc/configs/sim/demo/main.ini", path: "LIB:basic_sim.tcl -no_sim_spindle", raw_value: "LIB:basic_sim.tcl -no_sim_spindle", parse_status: "missing" },
{ resource_source_id: "r2", entrypoint: "linuxcnc/configs/sim/demo/main.ini", path: "missing.hal", raw_value: "missing.hal", parse_status: "missing" },
],
});
assert.equal(report.schema, "configuration-startup-closure.v1");
assert.equal(report.status, "blocked");
assert.equal(report.resolved_count, 1);
assert.equal(report.blocked_count, 1);
console.log(`configuration_resolver=passed resolved=${report.resolved_count} blocked=${report.blocked_count}`);

View File

@@ -0,0 +1,12 @@
import assert from "node:assert/strict";
import { preflightHalConfiguration } from "./hal-configuration.mjs";
const report = preflightHalConfiguration({
manifest: [{ component: "demo" }],
lines: ["loadrt demo names=d0", "addf d0 servo-thread", "loadusr -W external"],
});
assert.equal(report.schema, "hal-configuration-preflight.v1");
assert.equal(report.status, "blocked");
assert.equal(report.blocked_count, 1);
assert.equal(report.records[2].code, "hal_host_dynamic_loader");
console.log(`hal_preflight=passed records=${report.record_count} blocked=${report.blocked_count}`);