Implement configuration, runtime, frontend, and task governance closure
This commit is contained in:
145
web/workspace/configuration.mjs
Normal file
145
web/workspace/configuration.mjs
Normal 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 };
|
||||
Reference in New Issue
Block a user