Files
cnc_wams/web-rtcp-5axis-xyzbc-trt-sim-plan/tools/compare-xyzbc-trt-evidence.mjs
2026-07-02 23:53:51 -04:00

746 lines
32 KiB
JavaScript

import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const nativePath = process.argv[2] || resolve(projectRoot, "working/evidence/native-xyzbc-trt-evidence.json");
const webPath = process.argv[3] || resolve(projectRoot, "working/evidence/web-xyzbc-trt-evidence.json");
const outputPath = process.argv[4] || resolve(projectRoot, "working/evidence/compare-xyzbc-trt-evidence.json");
const PREVIEW_TCP_MAX_ERROR_MM = 0.001;
const PREVIEW_JOINT_MAX_ERROR = 0.001;
const PREVIEW_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const EXECUTION_TCP_MAX_ERROR_MM = 0.001;
const EXECUTION_JOINT_MAX_ERROR = 0.001;
const EXECUTION_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const nativeEvidence = JSON.parse(await readFile(nativePath, "utf8"));
const webEvidence = JSON.parse(await readFile(webPath, "utf8"));
const pathComparison = comparePathEvidence(nativeEvidence, webEvidence);
const lineExecutionComparison = compareLineExecutionTrace(nativeEvidence, webEvidence);
const axisValuesByLineComparison = compareAxisValuesByLine(nativeEvidence, webEvidence);
const gcodeExecutionProcessComparison = compareGcodeExecutionProcess(nativeEvidence, webEvidence);
const checks = [
check("profile", "native axis mask is XYZBC", nativeEvidence.coverage?.axisProfile === true, {
nativeAxisMask: nativeEvidence.after?.axisMask ?? nativeEvidence.before?.axisMask,
}),
check("profile", "web default profile is xyzbc-trt", webEvidence.coverage?.profileDefaultXyzbc === true, {
webProfile: webEvidence.profile?.id,
}),
check("ini", "native loaded xyzbc switchkins program", nativeEvidence.coverage?.xyzbcProgramOpen === true, {
nativeFile: nativeEvidence.after?.file ?? nativeEvidence.before?.file,
}),
check("ini", "web INI parse is ready", webEvidence.coverage?.iniReady === true, {
webIniReady: webEvidence.ini?.ready,
}),
check("switchkins", "native switchkins pin readable", nativeEvidence.coverage?.switchkinsPinReadable === true, {
nativePin: nativeEvidence.hal?.pins?.["motion.switchkins-type"],
}),
check("switchkins", "web remap files staged", webEvidence.coverage?.remapsStaged === true, {
remaps: webEvidence.ini?.remaps,
}),
check("program", "web default xyzbc_switchkins program staged", webEvidence.coverage?.defaultProgramStaged === true, {
selected: webEvidence.opfsStaging?.selectedProgram,
}),
check("tool-table", "web tool table staged", webEvidence.coverage?.toolTableStaged === true, {
toolTable: webEvidence.ini?.toolTable,
}),
check("tool-table", "web tool table active offset drives kinematics, path, and Vismach", webEvidence.coverage?.toolTableToToolOffsetClosed === true, {
toolRuntime: {
activeToolNumber: webEvidence.toolRuntime?.activeToolNumber,
activePocket: webEvidence.toolRuntime?.activePocket,
kinematicsToolOffsetZ: webEvidence.toolRuntime?.kinematics?.toolOffsetZ,
pathTool: webEvidence.toolRuntime?.pathTool,
vismachToolOffset: webEvidence.toolRuntime?.vismach?.toolOffset,
defaultActivationUsed: webEvidence.toolRuntime?.defaultActivationUsed,
},
toolOffsetClosure: webEvidence.toolOffsetClosure,
}),
check("pyvcp", "web PyVCP XML staged", webEvidence.coverage?.pyvcpXmlStaged === true, {
pyvcpXmlPath: webEvidence.profile?.pyvcpXmlPath,
}),
check("positions", "native joint feedback readable", nativeEvidence.coverage?.jointFeedbackReadable === true, {
joints: nativeEvidence.after?.jointActualPosition,
}),
check("wasm", "required WASM artifacts available", webEvidence.coverage?.wasmArtifactsReady === true, {
missing: webEvidence.wasm?.missing,
}),
check("parameters", "parameter file staged", webEvidence.coverage?.parameterFileStaged === true, {
parameterFile: webEvidence.ini?.parameterFile,
}),
check("program", "web boat-xyzbc demo program staged", webEvidence.coverage?.boatProgramStaged === true, {
demoPrograms: webEvidence.demoPrograms,
}),
check("ngcgui", "web Ngcgui/remap subroutines staged", webEvidence.coverage?.ngcguiSubroutinesStaged === true, {
ngcguiSubroutines: webEvidence.ngcguiSubroutines,
}),
check("ngcgui", "web Ngcgui/remap subroutines execute through WASM interpreter wrappers", webEvidence.coverage?.ngcguiSubroutinesExecutable === true, {
ngcguiExecution: webEvidence.ngcguiExecution,
}),
check("postgui-hal", "web PyVCP to HALUI POSTGUI nets represented", webEvidence.coverage?.postguiHalEquivalent === true, {
halNets: webEvidence.halNets,
}),
check("basic-sim", "native basic_sim joint/home/spindle/toolchange feedback readable", nativeEvidence.coverage?.basicSimReadable === true, {
basicSimEquivalent: nativeEvidence.basicSimEquivalent,
}),
check("basic-sim", "web task/HAL basic_sim equivalent covers joint/home/spindle/toolchange feedback", webEvidence.coverage?.basicSimEquivalent === true, {
basicSimEquivalent: webEvidence.basicSimEquivalent,
taskHalEquivalence: webEvidence.taskHalEquivalence,
}),
check("kinematics", "web kinematics HAL pins represented", webEvidence.coverage?.kinematicsPinsCovered === true, {
kinematicsPins: webEvidence.kinematicsPins,
}),
check("limits", "web TRAJ/AXIS/JOINT limits represented", webEvidence.coverage?.axisJointLimitsCovered === true, {
axisJointLimits: webEvidence.axisJointLimits,
}),
check("ui", "web AXIS first screen exposes program, coordinates, status, MDI/switchkins, override, tool, preview, and execution", webEvidence.coverage?.axisMainUiEquivalent === true, {
axisMainUi: webEvidence.axisMainUi,
}),
check("ui", "web AXIS/PyVCP buttons are source-referenced and covered by a button-level parity matrix", webEvidence.coverage?.axisButtonParityCovered === true, {
axisButtonParity: webEvidence.axisMainUi?.axisButtonParity,
}),
check("ui", "web UI state flow, buttons, HAL pins, and paths are rechecked against native xyzbc-trt runtime", webEvidence.coverage?.nativeStateFlowRechecked === true && nativeEvidence.coverage?.taskStateFlowReadable === true, {
nativeTaskStateFlow: nativeEvidence.taskStateFlow,
webNativeStateFlowReview: webEvidence.nativeStateFlowReview,
}),
check("path-preview", "native and web preview path sample period is 50ms", pathComparison.previewVsPreview.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.previewVsPreview.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.previewVsPreview.webSamplePeriodMs,
}),
check("path-preview", "native and web preview paths have samples", pathComparison.previewVsPreview.comparable === true, {
nativeSampleCount: pathComparison.previewVsPreview.nativeSampleCount,
webSampleCount: pathComparison.previewVsPreview.webSampleCount,
unavailable: pathComparison.previewVsPreview.unavailable,
}),
check("path-preview", "native and web preview tool paths are geometrically aligned", pathComparison.previewVsPreview.geometricAligned === true, {
thresholds: pathComparison.previewVsPreview.thresholds,
maxTcpErrorMm: pathComparison.previewVsPreview.maxTcpErrorMm,
rmsTcpErrorMm: pathComparison.previewVsPreview.rmsTcpErrorMm,
maxJointError: pathComparison.previewVsPreview.maxJointError,
maxToolAxisAngleDeg: pathComparison.previewVsPreview.maxToolAxisAngleDeg,
sampleCountDelta: pathComparison.previewVsPreview.sampleCountDelta,
}),
check("path-execution", "native and web execution path sample period is 50ms", pathComparison.executionVsExecution.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.executionVsExecution.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.executionVsExecution.webSamplePeriodMs,
}),
check("path-execution", "native and web execution paths have samples", pathComparison.executionVsExecution.comparable === true, {
nativeSampleCount: pathComparison.executionVsExecution.nativeSampleCount,
webSampleCount: pathComparison.executionVsExecution.webSampleCount,
unavailable: pathComparison.executionVsExecution.unavailable,
}),
check("path-execution", "native and web source-expanded execution paths are geometrically aligned", pathComparison.semanticExecutionVsSemanticExecution.geometricAligned === true, {
thresholds: pathComparison.semanticExecutionVsSemanticExecution.thresholds,
nativeSampleCount: pathComparison.semanticExecutionVsSemanticExecution.nativeSampleCount,
webSampleCount: pathComparison.semanticExecutionVsSemanticExecution.webSampleCount,
maxTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.maxTcpErrorMm,
rmsTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.rmsTcpErrorMm,
maxJointError: pathComparison.semanticExecutionVsSemanticExecution.maxJointError,
maxToolAxisAngleDeg: pathComparison.semanticExecutionVsSemanticExecution.maxToolAxisAngleDeg,
machineStateMismatchCount: pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount,
sampleCountDelta: pathComparison.semanticExecutionVsSemanticExecution.sampleCountDelta,
}),
check("line-execution", "native and web per-line G-code execution trace matches", lineExecutionComparison.status === "pass", {
nativeTraceCount: lineExecutionComparison.nativeTraceCount,
webTraceCount: lineExecutionComparison.webTraceCount,
mismatchCount: lineExecutionComparison.mismatchCount,
mismatches: lineExecutionComparison.mismatches.slice(0, 10),
}),
check("axis-values", "native and web actual axis values by executed line match", axisValuesByLineComparison.status === "pass", {
nativeLineValueCount: axisValuesByLineComparison.nativeLineValueCount,
webLineValueCount: axisValuesByLineComparison.webLineValueCount,
thresholds: axisValuesByLineComparison.thresholds,
maxTcpErrorMm: axisValuesByLineComparison.maxTcpErrorMm,
maxJointError: axisValuesByLineComparison.maxJointError,
maxToolAxisAngleDeg: axisValuesByLineComparison.maxToolAxisAngleDeg,
mismatchCount: axisValuesByLineComparison.mismatchCount,
mismatches: axisValuesByLineComparison.mismatches.slice(0, 10),
}),
check("gcode-process", "native and web complete G-code execution process JSON matches", gcodeExecutionProcessComparison.status === "pass", {
nativeExecutionStepCount: gcodeExecutionProcessComparison.nativeExecutionStepCount,
webExecutionStepCount: gcodeExecutionProcessComparison.webExecutionStepCount,
nativeSourceLineCoverageCount: gcodeExecutionProcessComparison.nativeSourceLineCoverageCount,
webSourceLineCoverageCount: gcodeExecutionProcessComparison.webSourceLineCoverageCount,
mismatchCount: gcodeExecutionProcessComparison.mismatchCount,
mismatches: gcodeExecutionProcessComparison.mismatches.slice(0, 10),
}),
check("path-preview-execution-consistency", "native preview and execution paths are comparable", pathComparison.previewVsExecutionNative.comparable === true, {
nativePreviewSampleCount: pathComparison.previewVsExecutionNative.leftSampleCount,
nativeExecutionSampleCount: pathComparison.previewVsExecutionNative.rightSampleCount,
unavailable: pathComparison.previewVsExecutionNative.unavailable,
}),
check("path-preview-execution-consistency", "web preview and execution paths are comparable", pathComparison.previewVsExecutionWeb.comparable === true, {
webPreviewSampleCount: pathComparison.previewVsExecutionWeb.leftSampleCount,
webExecutionSampleCount: pathComparison.previewVsExecutionWeb.rightSampleCount,
unavailable: pathComparison.previewVsExecutionWeb.unavailable,
}),
];
const failed = checks.filter((item) => item.status !== "pass");
const blockers = [
...(nativeEvidence.status === "blocked" ? nativeEvidence.blocker ? [nativeEvidence.blocker] : ["native-blocked"] : []),
...(webEvidence.blockers || []).map((blocker) => blocker.id),
];
const report = {
apiName: "xyzbc-trt-native-web-evidence-comparison",
status: failed.length === 0 ? "pass" : "fail",
comparedAt: new Date().toISOString(),
nativePath,
webPath,
summary: {
checkCount: checks.length,
passCount: checks.length - failed.length,
failCount: failed.length,
blockers,
nativeStatus: nativeEvidence.status,
webStatus: webEvidence.status,
},
checks,
pathComparison,
lineExecutionComparison,
axisValuesByLineComparison,
gcodeExecutionProcessComparison,
requiredImprovements: failed.map((item) => ({
category: item.category,
requirement: item.requirement,
evidence: item.evidence,
})),
semanticBoundary: "native_linuxcnc_vs_web_opfs_wasm_xyzbc_trt_evidence_comparison",
};
await mkdir(dirname(outputPath), { recursive: true });
await writeFile(outputPath, JSON.stringify(report, null, 2) + "\n", "utf8");
console.log(`compare_xyzbc_trt_evidence=${outputPath}`);
if (failed.length > 0) {
console.log(`compare_xyzbc_trt_status=fail fail_count=${failed.length}`);
} else {
console.log("compare_xyzbc_trt_status=pass");
}
function check(category, requirement, passed, evidence = {}) {
return {
category,
requirement,
status: passed ? "pass" : "fail",
evidence,
};
}
function comparePathEvidence(nativeEvidence, webEvidence) {
const samplePeriodMs = 50;
return {
samplePeriodMs,
previewVsPreview: compareNamedPaths({
left: nativeEvidence.previewPath,
right: webEvidence.previewPath,
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
}),
executionVsExecution: compareNamedPaths({
left: nativeEvidence.executionPath,
right: webEvidence.executionPath,
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
strictGeometry: false,
}),
semanticExecutionVsSemanticExecution: compareNamedPaths({
left: nativeEvidence.semanticExecutionPath,
right: webEvidence.semanticExecutionPath,
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
strictGeometry: true,
thresholds: {
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
maxJointError: EXECUTION_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
},
}),
previewVsExecutionNative: compareNamedPaths({
left: nativeEvidence.previewPath,
right: nativeEvidence.executionPath,
leftName: "nativePreview",
rightName: "nativeExecution",
expectedSamplePeriodMs: samplePeriodMs,
}),
previewVsExecutionWeb: compareNamedPaths({
left: webEvidence.previewPath,
right: webEvidence.executionPath,
leftName: "webPreview",
rightName: "webExecution",
expectedSamplePeriodMs: samplePeriodMs,
}),
};
}
function compareNamedPaths({
left,
right,
leftName,
rightName,
expectedSamplePeriodMs,
strictGeometry = leftName === "native" && rightName === "web",
thresholds = null,
}) {
const leftSamplePeriodMs = left?.samplePeriodMs ?? null;
const rightSamplePeriodMs = right?.samplePeriodMs ?? null;
const periodsMatch = leftSamplePeriodMs === expectedSamplePeriodMs
&& rightSamplePeriodMs === expectedSamplePeriodMs;
const leftSamples = Array.isArray(left?.samples) ? left.samples : [];
const rightSamples = Array.isArray(right?.samples) ? right.samples : [];
const comparable = periodsMatch && leftSamples.length > 0 && rightSamples.length > 0;
const unavailable = [
...(leftSamples.length > 0 ? [] : [`${leftName}: ${left?.unavailableReason || "missing samples"}`]),
...(rightSamples.length > 0 ? [] : [`${rightName}: ${right?.unavailableReason || "missing samples"}`]),
...(periodsMatch ? [] : [`sample period mismatch ${leftSamplePeriodMs}/${rightSamplePeriodMs}`]),
];
const stats = comparable ? pathStats(leftSamples, rightSamples) : emptyStats(leftSamples, rightSamples);
const resolvedThresholds = strictGeometry ? (thresholds || {
maxTcpErrorMm: PREVIEW_TCP_MAX_ERROR_MM,
maxJointError: PREVIEW_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: PREVIEW_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
}) : null;
const geometricAligned = strictGeometry
? comparable
&& stats.maxTcpErrorMm <= resolvedThresholds.maxTcpErrorMm
&& stats.maxJointError <= resolvedThresholds.maxJointError
&& stats.maxToolAxisAngleDeg <= resolvedThresholds.maxToolAxisAngleDeg
&& stats.sampleCountDelta <= resolvedThresholds.sampleCountDelta
&& stats.machineStateMismatchCount === 0
&& stats.missingSamples.length === 0
: comparable;
return {
status: comparable && (!strictGeometry || geometricAligned) ? "pass" : "fail",
comparable,
geometricAligned,
thresholds: resolvedThresholds,
periodsMatch,
[`${leftName}SamplePeriodMs`]: leftSamplePeriodMs,
[`${rightName}SamplePeriodMs`]: rightSamplePeriodMs,
nativeSamplePeriodMs: leftName === "native" ? leftSamplePeriodMs : undefined,
webSamplePeriodMs: rightName === "web" ? rightSamplePeriodMs : undefined,
leftSampleCount: leftSamples.length,
rightSampleCount: rightSamples.length,
nativeSampleCount: leftName === "native" ? leftSamples.length : undefined,
webSampleCount: rightName === "web" ? rightSamples.length : undefined,
unavailable,
...stats,
};
}
function compareLineExecutionTrace(nativeEvidence, webEvidence) {
const nativeTrace = Array.isArray(nativeEvidence.lineExecutionTrace)
? nativeEvidence.lineExecutionTrace
: nativeEvidence.semanticExecutionPath?.lineExecutionTrace || [];
const webTrace = Array.isArray(webEvidence.lineExecutionTrace)
? webEvidence.lineExecutionTrace
: webEvidence.semanticExecutionPath?.lineExecutionTrace || [];
const count = Math.min(nativeTrace.length, webTrace.length);
const mismatches = [];
for (let index = 0; index < count; index += 1) {
const left = nativeTrace[index];
const right = webTrace[index];
const keys = ["sourceFile", "line", "operation", "motionType", "activeKinematicsAfter", "producesMotion", "segmentIndex"];
const differences = keys
.filter((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]))
.map((key) => ({ key, native: left?.[key], web: right?.[key] }));
if (differences.length > 0) {
mismatches.push({
index,
native: projectTraceEntry(left),
web: projectTraceEntry(right),
differences,
});
}
}
if (nativeTrace.length !== webTrace.length) {
mismatches.push({
index: count,
differences: [{
key: "traceCount",
native: nativeTrace.length,
web: webTrace.length,
}],
});
}
return {
status: mismatches.length === 0 && nativeTrace.length > 0 && webTrace.length > 0 ? "pass" : "fail",
nativeTraceCount: nativeTrace.length,
webTraceCount: webTrace.length,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_line_by_line_gcode_execution_trace_comparison",
};
}
function compareAxisValuesByLine(nativeEvidence, webEvidence) {
const nativeValues = Array.isArray(nativeEvidence.axisValuesByLine)
? nativeEvidence.axisValuesByLine
: nativeEvidence.semanticExecutionPath?.axisValuesByLine || [];
const webValues = Array.isArray(webEvidence.axisValuesByLine)
? webEvidence.axisValuesByLine
: webEvidence.semanticExecutionPath?.axisValuesByLine || [];
const count = Math.min(nativeValues.length, webValues.length);
const mismatches = [];
let maxTcpErrorMm = 0;
let maxJointError = 0;
let maxToolAxisAngleDeg = 0;
const thresholds = {
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
maxJointError: EXECUTION_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
};
for (let index = 0; index < count; index += 1) {
const left = nativeValues[index];
const right = webValues[index];
const tcpError = vectorError(left?.tcp, right?.tcp, ["x", "y", "z"]);
const jointError = vectorError(left?.joint, right?.joint, ["x", "y", "z", "b", "c"]);
const angleError = toolAxisAngleDeg(left?.toolAxis, right?.toolAxis);
const machineStateMatches = compareJsonStable(left?.machineState, right?.machineState);
maxTcpErrorMm = Math.max(maxTcpErrorMm, tcpError);
maxJointError = Math.max(maxJointError, jointError);
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
const identityMismatch = ["sourceFile", "line", "operation", "motionType", "activeKinematics", "segmentIndex"]
.some((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]));
if (identityMismatch
|| tcpError > thresholds.maxTcpErrorMm
|| jointError > thresholds.maxJointError
|| angleError > thresholds.maxToolAxisAngleDeg
|| !machineStateMatches) {
mismatches.push({
index,
native: projectAxisValue(left),
web: projectAxisValue(right),
tcpError,
jointError,
toolAxisAngleDeg: angleError,
machineStateMatches,
});
}
}
if (nativeValues.length !== webValues.length) {
mismatches.push({
index: count,
nativeLineValueCount: nativeValues.length,
webLineValueCount: webValues.length,
});
}
return {
status: mismatches.length === 0 && nativeValues.length > 0 && webValues.length > 0 ? "pass" : "fail",
nativeLineValueCount: nativeValues.length,
webLineValueCount: webValues.length,
thresholds,
maxTcpErrorMm,
maxJointError,
maxToolAxisAngleDeg,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_executed_line_axis_values_comparison",
};
}
function compareGcodeExecutionProcess(nativeEvidence, webEvidence) {
const nativeProcess = nativeEvidence.gcodeExecutionProcess || nativeEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
const webProcess = webEvidence.gcodeExecutionProcess || webEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
const nativeSteps = Array.isArray(nativeProcess?.executionSteps) ? nativeProcess.executionSteps : [];
const webSteps = Array.isArray(webProcess?.executionSteps) ? webProcess.executionSteps : [];
const nativeCoverage = Array.isArray(nativeProcess?.sourceLineCoverage) ? nativeProcess.sourceLineCoverage : [];
const webCoverage = Array.isArray(webProcess?.sourceLineCoverage) ? webProcess.sourceLineCoverage : [];
const mismatches = [];
if (nativeProcess?.status !== "ok" || webProcess?.status !== "ok") {
mismatches.push({
index: 0,
field: "status",
native: nativeProcess?.status ?? null,
web: webProcess?.status ?? null,
});
}
compareCoverage(nativeCoverage, webCoverage, mismatches);
const stepCount = Math.min(nativeSteps.length, webSteps.length);
for (let index = 0; index < stepCount; index += 1) {
const left = nativeSteps[index];
const right = webSteps[index];
const differences = [];
for (const key of ["stepIndex", "sourceFile", "line", "statement", "callDepth", "executed", "sourceLineKind"]) {
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
differences.push({ key, native: left?.[key], web: right?.[key] });
}
}
for (const key of [
"operation",
"sourceLineKind",
"executed",
"activeKinematicsBefore",
"activeKinematicsAfter",
"traceExecutionIndex",
]) {
if (normalizeComparable(left?.result?.[key]) !== normalizeComparable(right?.result?.[key])) {
differences.push({ key: `result.${key}`, native: left?.result?.[key], web: right?.result?.[key] });
}
}
if (!compareJsonStable(left?.result?.machineStateAfter, right?.result?.machineStateAfter)) {
differences.push({
key: "result.machineStateAfter",
native: left?.result?.machineStateAfter,
web: right?.result?.machineStateAfter,
});
}
if (JSON.stringify(left?.result?.parametersChanged || {}) !== JSON.stringify(right?.result?.parametersChanged || {})) {
differences.push({
key: "result.parametersChanged",
native: left?.result?.parametersChanged,
web: right?.result?.parametersChanged,
});
}
const leftMotion = left?.result?.motion || null;
const rightMotion = right?.result?.motion || null;
if (Boolean(leftMotion) !== Boolean(rightMotion)) {
differences.push({ key: "result.motion", native: Boolean(leftMotion), web: Boolean(rightMotion) });
} else if (leftMotion && rightMotion) {
const tcpError = vectorError(leftMotion.endTcp, rightMotion.endTcp, ["x", "y", "z"]);
const jointError = vectorError(leftMotion.endJoint, rightMotion.endJoint, ["x", "y", "z", "b", "c"]);
const axisError = toolAxisAngleDeg(leftMotion.endToolAxis, rightMotion.endToolAxis);
if (tcpError > EXECUTION_TCP_MAX_ERROR_MM || jointError > EXECUTION_JOINT_MAX_ERROR || axisError > EXECUTION_TOOL_AXIS_MAX_ERROR_DEG) {
differences.push({
key: "result.motion.axisValues",
tcpError,
jointError,
toolAxisAngleDeg: axisError,
native: projectMotion(leftMotion),
web: projectMotion(rightMotion),
});
}
for (const key of ["segmentIndex", "motionType", "feed"]) {
if (normalizeComparable(leftMotion[key]) !== normalizeComparable(rightMotion[key])) {
differences.push({ key: `result.motion.${key}`, native: leftMotion[key], web: rightMotion[key] });
}
}
}
if (differences.length > 0) {
mismatches.push({
index,
native: projectGcodeStep(left),
web: projectGcodeStep(right),
differences,
});
}
}
if (nativeSteps.length !== webSteps.length) {
mismatches.push({
index: stepCount,
field: "executionSteps.length",
native: nativeSteps.length,
web: webSteps.length,
});
}
return {
status: mismatches.length === 0 && nativeSteps.length > 0 && webSteps.length > 0 ? "pass" : "fail",
nativeExecutionStepCount: nativeSteps.length,
webExecutionStepCount: webSteps.length,
nativeSourceLineCoverageCount: nativeCoverage.length,
webSourceLineCoverageCount: webCoverage.length,
nativeSummary: nativeProcess?.summary || null,
webSummary: webProcess?.summary || null,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_complete_gcode_execution_process_json_comparison",
};
}
function compareCoverage(nativeCoverage, webCoverage, mismatches) {
const count = Math.min(nativeCoverage.length, webCoverage.length);
for (let index = 0; index < count; index += 1) {
const left = nativeCoverage[index];
const right = webCoverage[index];
const differences = [];
for (const key of ["sourceFile", "line", "statement", "sourceLineKind", "visitCount", "producedMotionCount"]) {
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
differences.push({ key: `coverage.${key}`, native: left?.[key], web: right?.[key] });
}
}
if (JSON.stringify(left?.operations || []) !== JSON.stringify(right?.operations || [])) {
differences.push({ key: "coverage.operations", native: left?.operations, web: right?.operations });
}
if (differences.length > 0) {
mismatches.push({
index,
field: "sourceLineCoverage",
native: left,
web: right,
differences,
});
}
}
if (nativeCoverage.length !== webCoverage.length) {
mismatches.push({
index: count,
field: "sourceLineCoverage.length",
native: nativeCoverage.length,
web: webCoverage.length,
});
}
}
function projectGcodeStep(step = {}) {
return {
stepIndex: step.stepIndex,
sourceFile: step.sourceFile,
line: step.line,
statement: step.statement,
callDepth: step.callDepth,
operation: step.result?.operation,
sourceLineKind: step.sourceLineKind,
activeKinematicsAfter: step.result?.activeKinematicsAfter,
traceExecutionIndex: step.result?.traceExecutionIndex,
motion: step.result?.motion ? projectMotion(step.result.motion) : null,
parametersChanged: step.result?.parametersChanged,
machineStateAfter: step.result?.machineStateAfter,
};
}
function projectMotion(motion = {}) {
return {
segmentIndex: motion.segmentIndex,
motionType: motion.motionType,
feed: motion.feed,
endJoint: motion.endJoint,
endTcp: motion.endTcp,
endToolAxis: motion.endToolAxis,
};
}
function projectTraceEntry(entry = {}) {
return {
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematicsAfter: entry.activeKinematicsAfter,
producesMotion: entry.producesMotion,
segmentIndex: entry.segmentIndex,
};
}
function projectAxisValue(entry = {}) {
return {
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematics: entry.activeKinematics,
segmentIndex: entry.segmentIndex,
joint: entry.joint,
tcp: entry.tcp,
toolAxis: entry.toolAxis,
machineState: entry.machineState,
};
}
function normalizeComparable(value) {
if (value === undefined || value === null) return null;
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
return value;
}
function pathStats(leftSamples, rightSamples) {
const count = Math.min(leftSamples.length, rightSamples.length);
const missingSamples = [];
let maxTcpErrorMm = 0;
let sumTcpErrorSquared = 0;
let maxJointError = 0;
let sumJointErrorSquared = 0;
let maxToolAxisAngleDeg = 0;
const machineStateMismatches = [];
for (let index = 0; index < count; index += 1) {
const left = leftSamples[index];
const right = rightSamples[index];
if (left.sampleIndex !== right.sampleIndex || left.timeMs !== right.timeMs) {
missingSamples.push({ index, leftSampleIndex: left.sampleIndex, rightSampleIndex: right.sampleIndex });
}
const tcpError = vectorError(left.tcp, right.tcp, ["x", "y", "z"]);
const jointError = vectorError(left.joint, right.joint, ["x", "y", "z", "b", "c"]);
const angleError = toolAxisAngleDeg(left.toolAxis, right.toolAxis);
maxTcpErrorMm = Math.max(maxTcpErrorMm, tcpError);
sumTcpErrorSquared += tcpError ** 2;
maxJointError = Math.max(maxJointError, jointError);
sumJointErrorSquared += jointError ** 2;
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
if (!compareJsonStable(left.machineState, right.machineState)) {
machineStateMismatches.push({
index,
native: left.machineState,
web: right.machineState,
});
}
}
return {
maxTcpErrorMm,
rmsTcpErrorMm: count > 0 ? Math.sqrt(sumTcpErrorSquared / count) : 0,
maxJointError,
rmsJointError: count > 0 ? Math.sqrt(sumJointErrorSquared / count) : 0,
maxToolAxisAngleDeg,
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
missingSamples,
machineStateMismatchCount: machineStateMismatches.length,
machineStateMismatches: machineStateMismatches.slice(0, 10),
};
}
function emptyStats(leftSamples, rightSamples) {
return {
maxTcpErrorMm: null,
rmsTcpErrorMm: null,
maxJointError: null,
rmsJointError: null,
maxToolAxisAngleDeg: null,
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
missingSamples: [],
machineStateMismatchCount: null,
machineStateMismatches: [],
};
}
function compareJsonStable(left, right) {
return JSON.stringify(normalizeForJsonCompare(left)) === JSON.stringify(normalizeForJsonCompare(right));
}
function normalizeForJsonCompare(value) {
if (value === undefined || value === null) return null;
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
if (Array.isArray(value)) return value.map((item) => normalizeForJsonCompare(item));
if (typeof value === "object") {
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalizeForJsonCompare(value[key])]));
}
return value;
}
function vectorError(left = {}, right = {}, keys = []) {
return Math.sqrt(keys.reduce((sum, key) => (
sum + (numberOrZero(left[key]) - numberOrZero(right[key])) ** 2
), 0));
}
function toolAxisAngleDeg(left = {}, right = {}) {
const dot = numberOrZero(left.i) * numberOrZero(right.i)
+ numberOrZero(left.j) * numberOrZero(right.j)
+ numberOrZero(left.k) * numberOrZero(right.k);
const leftLen = vectorError(left, { i: 0, j: 0, k: 0 }, ["i", "j", "k"]);
const rightLen = vectorError(right, { i: 0, j: 0, k: 0 }, ["i", "j", "k"]);
if (leftLen <= 0 || rightLen <= 0) return 0;
const cosine = Math.max(-1, Math.min(1, dot / (leftLen * rightLen)));
return Math.acos(cosine) * 180 / Math.PI;
}
function numberOrZero(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}