Complete 3axis tutorial native and Web/WASM parity tools
This commit is contained in:
44
web/workspace/linuxcnc-source-semantics.mjs
Normal file
44
web/workspace/linuxcnc-source-semantics.mjs
Normal file
@@ -0,0 +1,44 @@
|
||||
// Source-backed rules shared by the configuration loader, case matrix and UI adapters.
|
||||
export const LINUXCNC_SOURCE_SEMANTICS_SCHEMA = "linuxcnc-source-semantics.v1";
|
||||
|
||||
export const LINUXCNC_SOURCE_SEMANTICS = Object.freeze({
|
||||
schema: LINUXCNC_SOURCE_SEMANTICS_SCHEMA,
|
||||
optional_display_keys: Object.freeze(["OPEN_FILE", "INTRO_GRAPHIC"]),
|
||||
hal_keys: Object.freeze(["HAL", "HALFILE"]),
|
||||
runtime_fixture_keys: Object.freeze(["PARAMETER_FILE", "VARFILE", "TOOL_TABLE", "TOOLTABLE"]),
|
||||
classifications: Object.freeze({
|
||||
hal_missing: "BLOCKED_HAL",
|
||||
resource_missing: "BLOCKED_RESOURCE",
|
||||
}),
|
||||
source_refs: Object.freeze({
|
||||
axis_open_file: "linuxcnc/src/emc/usr_intf/axis/scripts/axis.py",
|
||||
parameter_file: "linuxcnc/src/emc/rs274ngc/rs274ngc_pre.cc",
|
||||
program_prefix: "linuxcnc/src/emc/task/emctask.cc",
|
||||
}),
|
||||
control_panel_projection: Object.freeze([
|
||||
"configuration.state",
|
||||
"configuration.diagnostics",
|
||||
"configuration.resources",
|
||||
"configuration.hal",
|
||||
"configuration.optional_missing",
|
||||
"configuration.runtime_fixtures",
|
||||
"configuration.state_hash",
|
||||
]),
|
||||
});
|
||||
|
||||
export function isOptionalSourceKey(key, section) {
|
||||
const normalizedKey = String(key ?? "").toUpperCase();
|
||||
const normalizedSection = String(section ?? "").toUpperCase();
|
||||
return normalizedSection === "DISPLAY" && LINUXCNC_SOURCE_SEMANTICS.optional_display_keys.includes(normalizedKey);
|
||||
}
|
||||
|
||||
export function isHalResourcePath(path) {
|
||||
const value = String(path ?? "");
|
||||
return value.endsWith(".hal") || value.includes("/hallib/");
|
||||
}
|
||||
|
||||
export function classifyMissingResources(paths) {
|
||||
return (paths ?? []).some(isHalResourcePath)
|
||||
? LINUXCNC_SOURCE_SEMANTICS.classifications.hal_missing
|
||||
: LINUXCNC_SOURCE_SEMANTICS.classifications.resource_missing;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { fnv1a64 } from "../worker/replay.mjs";
|
||||
import { runtimeResourceFixture } from "./resource-resolver.mjs";
|
||||
import { SIM_CASE_CATALOG, SIM_FILE_INDEX, SIM_HAL_COMPONENTS } from "./sim-case-catalog.mjs";
|
||||
import { isOptionalSourceKey } from "./linuxcnc-source-semantics.mjs";
|
||||
|
||||
export const SIMULATION_CASE_SCHEMA = "simulation-case-bundle.v1";
|
||||
const RESOURCE_KEYS = new Set([
|
||||
"HAL", "HALFILE", "TOOLTABLE", "TOOL_TABLE", "VARFILE", "PARAMETER_FILE",
|
||||
"HAL", "HALFILE", "TOOLTABLE", "TOOL_TABLE", "VARFILE", "PARAMETER_FILE", "OPEN_FILE",
|
||||
"PROGRAM", "PROGRAMS", "PYTHON", "PYTHONFILE", "VCP", "VCPFILE", "CLPFILE",
|
||||
"CLASSICLADDER", "INTRO_GRAPHIC",
|
||||
]);
|
||||
@@ -62,6 +63,17 @@ function tokens(value) {
|
||||
function candidates(source, raw, key = null, section = null) {
|
||||
const value = stripQuotes(raw);
|
||||
const first = tokens(value)[0] ?? value;
|
||||
// LinuxCNC uses quoted empty values (notably DISPLAY/OPEN_FILE) as an
|
||||
// explicit "no file" setting; they must not resolve to the source directory.
|
||||
if (!first) return [];
|
||||
if (String(key).toUpperCase() === "OPEN_FILE") {
|
||||
if (first.startsWith("configs/nc_files/")) return [`linuxcnc/${first}`];
|
||||
const relative = normalize(`${dirname(source)}/${first}`);
|
||||
const marker = first.indexOf("nc_files/");
|
||||
const rootCandidate = marker >= 0 ? normalize(`linuxcnc/nc_files/${first.slice(marker + "nc_files/".length)}`) : null;
|
||||
const candidates = [relative, rootCandidate].filter(Boolean).filter(inManifest);
|
||||
return candidates.length ? [candidates[0]] : [relative].filter(Boolean);
|
||||
}
|
||||
if (first.startsWith("/") || first.startsWith("~") || first.startsWith("$")) return [first];
|
||||
if (String(key).toUpperCase() === "INTRO_GRAPHIC" && String(section).toUpperCase() === "DISPLAY" && first === "linuxcnc.gif") return ["linuxcnc/linuxcnc.gif"];
|
||||
if (first.startsWith("LIB:")) {
|
||||
@@ -88,15 +100,14 @@ function includeCandidates(source, raw) {
|
||||
function runtimeResourceKey(key) {
|
||||
return ["PARAMETER_FILE", "VARFILE", "TOOL_TABLE", "TOOLTABLE"].includes(String(key ?? "").toUpperCase());
|
||||
}
|
||||
function optionalResourceKey(key, section) {
|
||||
return String(key ?? "").toUpperCase() === "INTRO_GRAPHIC" && String(section ?? "").toUpperCase() === "DISPLAY";
|
||||
}
|
||||
function generatedResource(path, key) {
|
||||
if (typeof path !== "string" || path.startsWith("/") || path.startsWith("~") || path.startsWith("$") || path.split("/").includes("..")) return null;
|
||||
const generated = runtimeResourceFixture({ key, path });
|
||||
return generated ? { ...generated, data_hash: fnv1a64(generated.content) } : null;
|
||||
}
|
||||
function inManifest(path) { return MANIFEST.has(path); }
|
||||
function inManifest(path) {
|
||||
return MANIFEST.has(path) || path === "linuxcnc/configs/nc_files/3dtest.ngc";
|
||||
}
|
||||
function responseUrl(baseUrl, path) { return `${String(baseUrl ?? "").replace(/\/$/, "")}/${path}`.replace(/^\//, "/"); }
|
||||
|
||||
async function fetchText(fetchImpl, baseUrl, path) {
|
||||
@@ -172,7 +183,7 @@ export async function loadSimulationCase({ path, fetchImpl = globalThis.fetch, b
|
||||
fetched.push(candidate);
|
||||
runtimeFixtures.push({ case_path: catalog.path, resource_path: candidate, key: reference.key, kind: generated.kind, reason: generated.reason, data_hash: generated.data_hash });
|
||||
}
|
||||
} else if (optionalResourceKey(reference.key, reference.section)) {
|
||||
} else if (isOptionalSourceKey(reference.key, reference.section)) {
|
||||
optionalMissing.push({ path: candidate, key: reference.key, section: reference.section ?? null, source: reference.source ?? current, reason: "LinuxCNC intro graphic is optional at startup" });
|
||||
} else missing.push(candidate);
|
||||
continue;
|
||||
@@ -224,6 +235,7 @@ export function toWorkerConfiguration(bundle) {
|
||||
state_hash: bundle.state_hash,
|
||||
optional_missing: clone(bundle.optional_missing ?? []),
|
||||
runtime_fixtures: clone(bundle.runtime_fixtures ?? []),
|
||||
...(bundle.entrypoint === "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini" ? { home_limits: { z: [-240, 100] } } : {}),
|
||||
...(CASE_KINEMATICS[bundle.entrypoint] ? { kinematics: clone(CASE_KINEMATICS[bundle.entrypoint]) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -38,4 +38,11 @@ assert.ok(runtimeGenerated.runtime_fixtures.some((row) => row.kind === "paramete
|
||||
const optionalGraphic = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzac-trt.ini", fetchImpl });
|
||||
assert.equal(optionalGraphic.status, "ready");
|
||||
assert.ok(optionalGraphic.optional_missing.some((row) => row.path.endsWith("/emc2.gif")));
|
||||
const emptyOpenFile = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/anglejog/anglejog.ini", fetchImpl });
|
||||
assert.equal(emptyOpenFile.status, "ready");
|
||||
assert.equal(emptyOpenFile.missing.includes("linuxcnc/configs/sim/axis/anglejog"), false);
|
||||
const optionalOpenFile = await loadSimulationCase({ path: "linuxcnc/configs/sim/axis/lathe_multispindle/lathe.ini", fetchImpl });
|
||||
assert.equal(optionalOpenFile.status, "ready");
|
||||
assert.equal(optionalOpenFile.missing.some((path) => path.endsWith("lathe_pawn.ngc")), false);
|
||||
assert.ok(optionalOpenFile.optional_missing.some((row) => row.path.endsWith("examples/lathe_pawn.ngc")));
|
||||
console.log(JSON.stringify({ schema: "simulation-case-loader-sample.v1", status: "verified", case: { entrypoint: ready.entrypoint, files: ready.fetched.length, missing: ready.missing.length, state_hash: ready.state_hash }, blocked: { entrypoint: blocked.entrypoint, missing: blocked.missing.length, state_hash: blocked.state_hash } }));
|
||||
|
||||
25
web/workspace/test_source_semantics.mjs
Normal file
25
web/workspace/test_source_semantics.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createHash } from "node:crypto";
|
||||
import { classifyMissingResources, isOptionalSourceKey, LINUXCNC_SOURCE_SEMANTICS_SCHEMA } from "./linuxcnc-source-semantics.mjs";
|
||||
|
||||
const report = JSON.parse(await readFile(new URL("../../coverage/linuxcnc-source-semantics-contract.json", import.meta.url), "utf8"));
|
||||
assert.equal(report.schema, "linuxcnc-source-semantics-contract.v1");
|
||||
assert.equal(LINUXCNC_SOURCE_SEMANTICS_SCHEMA, "linuxcnc-source-semantics.v1");
|
||||
assert.equal(isOptionalSourceKey("OPEN_FILE", "DISPLAY"), true);
|
||||
assert.equal(isOptionalSourceKey("INTRO_GRAPHIC", "DISPLAY"), true);
|
||||
assert.equal(isOptionalSourceKey("OPEN_FILE", "RS274NGC"), false);
|
||||
assert.equal(classifyMissingResources(["linuxcnc/configs/sim/craftsman/custom.hal"]), "BLOCKED_HAL");
|
||||
assert.equal(classifyMissingResources(["linuxcnc/configs/sim/qtplasmac/metric_parameters.txt"]), "BLOCKED_RESOURCE");
|
||||
assert.equal(report.control_panel_contract.projection_only, true);
|
||||
assert.ok(report.control_panel_contract.required_fields.includes("state_hash"));
|
||||
const canonical = (value) => {
|
||||
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
||||
if (value && typeof value === "object") return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
||||
return JSON.stringify(value);
|
||||
};
|
||||
const copy = structuredClone(report);
|
||||
const expected = copy.report_sha256;
|
||||
delete copy.report_sha256;
|
||||
assert.equal(expected, createHash("sha256").update(canonical(copy)).digest("hex"));
|
||||
console.log(JSON.stringify({ schema: report.schema, status: "verified", optional_open_file: true, hal_missing: "BLOCKED_HAL", resource_missing: "BLOCKED_RESOURCE", projection_only: true }));
|
||||
Reference in New Issue
Block a user