1137 lines
54 KiB
JavaScript
1137 lines
54 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 strictComparison = compareStrictEvidence(nativeEvidence, webEvidence);
|
|
const functionalChecks = compareFunctionalEvidence(nativeEvidence, webEvidence, {
|
|
pathComparison,
|
|
lineExecutionComparison,
|
|
axisValuesByLineComparison,
|
|
gcodeExecutionProcessComparison,
|
|
strictComparison,
|
|
});
|
|
|
|
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,
|
|
}),
|
|
check("source-manifest", "T-051 LinuxCNC source tree authority manifest is complete", strictComparison.sourceManifestComparison.status === "pass", strictComparison.sourceManifestComparison),
|
|
check("runtime-launch", "T-052 native/Web launch entrypoints and runtime environment are recorded", strictComparison.runtimeLaunchComparison.status === "pass", strictComparison.runtimeLaunchComparison),
|
|
check("ini-full", "T-053 complete INI section/key coverage matches native baseline", strictComparison.iniFullComparison.status === "pass", strictComparison.iniFullComparison),
|
|
check("hal-graph", "T-054 HAL source graph and runtime pin model are present", strictComparison.halGraphComparison.status === "pass", strictComparison.halGraphComparison),
|
|
check("kinematics", "T-055 xyzbc-trt kinematics formula evidence and sample validation are present", strictComparison.kinematicsFormulaComparison.status === "pass", strictComparison.kinematicsFormulaComparison),
|
|
check("remap", "T-056 M428/M429/M430 remap semantics are source-checked", strictComparison.remapSemanticsComparison.status === "pass", strictComparison.remapSemanticsComparison),
|
|
check("pyvcp-postgui", "T-057 PyVCP POSTGUI HAL full chain is represented", strictComparison.pyvcpPostguiComparison.status === "pass", strictComparison.pyvcpPostguiComparison),
|
|
check("axis-ui", "T-058 AXIS UI source behavior references are represented", strictComparison.axisUiBehaviorComparison.status === "pass", strictComparison.axisUiBehaviorComparison),
|
|
check("vismach", "T-059 Vismach transform tree evidence is represented", strictComparison.visualComparison.status === "pass", strictComparison.visualComparison),
|
|
check("timing", "T-060 servo/task timing and 50ms sampling budget are represented", strictComparison.servoTaskTimingComparison.status === "pass", strictComparison.servoTaskTimingComparison),
|
|
check("runtime-execution", "T-061 true runtime execution samples are separated from source-derived expansion", strictComparison.runtimeExecutionComparison.status === "pass", strictComparison.runtimeExecutionComparison),
|
|
check("staging-hash", "T-062 Web staged file hashes match native source manifest for staged files", strictComparison.webStagingHashComparison.status === "pass", strictComparison.webStagingHashComparison),
|
|
check("wasm-source", "T-063 WASM artifacts are bound to source and have hashes", strictComparison.wasmSourceBindingComparison.status === "pass", strictComparison.wasmSourceBindingComparison),
|
|
check("task-hal-full", "T-064 task/HAL full state fields are represented", strictComparison.taskHalFullStateComparison.status === "pass", strictComparison.taskHalFullStateComparison),
|
|
check("limits", "T-065 TRAJ/AXIS/JOINT limits and interlocks are represented", strictComparison.limitInterlocksComparison.status === "pass", strictComparison.limitInterlocksComparison),
|
|
check("tool-parameters", "T-066 tool table and parameter file persistence are represented", strictComparison.toolParameterComparison.status === "pass", strictComparison.toolParameterComparison),
|
|
check("program-corpus", "T-067 Ngcgui and demo program corpus execution is represented", strictComparison.programCorpusComparison.status === "pass", strictComparison.programCorpusComparison),
|
|
check("visual", "T-068 native/Web visual evidence paths are present", strictComparison.nativeWebVisualComparison.status === "pass", strictComparison.nativeWebVisualComparison),
|
|
check("errors", "T-069 error path parity matrix is represented", strictComparison.errorPathComparison.status === "pass", strictComparison.errorPathComparison),
|
|
check("dual-baseline", "T-070 compare JSON contains source/runtime dual baseline sections", strictComparison.dualBaselineComparison.status === "pass", strictComparison.dualBaselineComparison),
|
|
check("classification", "T-071 evidence classification prevents static derivation being labeled runtime", strictComparison.evidenceClassificationComparison.status === "pass", strictComparison.evidenceClassificationComparison),
|
|
check("rerun", "T-072 one-command rerun entrypoint is recorded", strictComparison.rerunEntryComparison.status === "pass", strictComparison.rerunEntryComparison),
|
|
check("reverse-index", "T-073 reverse source index is present", strictComparison.reverseSourceIndexComparison.status === "pass", strictComparison.reverseSourceIndexComparison),
|
|
check("performance", "T-074 performance/error budget is represented and current geometric errors fit", strictComparison.performanceBudgetComparison.status === "pass", strictComparison.performanceBudgetComparison),
|
|
check("strict-acceptance", "T-075 strict acceptance freeze metadata is present", strictComparison.strictAcceptanceComparison.status === "pass", strictComparison.strictAcceptanceComparison),
|
|
];
|
|
|
|
const failed = checks.filter((item) => item.status !== "pass");
|
|
const functionalFailed = functionalChecks.filter((item) => item.status !== "pass");
|
|
const blockers = [
|
|
...(nativeEvidence.status === "blocked" ? nativeEvidence.blocker ? [nativeEvidence.blocker] : ["native-blocked"] : []),
|
|
...(webEvidence.blockers || []).map((blocker) => blocker.id),
|
|
];
|
|
const surfaceSummary = {
|
|
checkCount: checks.length,
|
|
passCount: checks.length - failed.length,
|
|
failCount: failed.length,
|
|
legacyComparePassCount: checks.length - failed.length,
|
|
legacyCompareFailCount: failed.length,
|
|
blockers,
|
|
nativeStatus: nativeEvidence.status,
|
|
webStatus: webEvidence.status,
|
|
};
|
|
const functionalSummary = {
|
|
status: functionalFailed.length === 0 ? "pass" : "fail",
|
|
checkCount: functionalChecks.length,
|
|
passCount: functionalChecks.length - functionalFailed.length,
|
|
failCount: functionalFailed.length,
|
|
requiredImprovements: functionalFailed.map((item) => ({
|
|
category: item.category,
|
|
requirement: item.requirement,
|
|
evidence: item.evidence,
|
|
})),
|
|
};
|
|
|
|
const report = {
|
|
apiName: "xyzbc-trt-native-web-evidence-comparison",
|
|
status: failed.length === 0 && functionalFailed.length === 0 ? "pass" : "fail",
|
|
comparedAt: new Date().toISOString(),
|
|
nativePath,
|
|
webPath,
|
|
summary: surfaceSummary,
|
|
surfaceSummary,
|
|
functionalSummary,
|
|
checks,
|
|
functionalChecks,
|
|
pathComparison,
|
|
lineExecutionComparison,
|
|
axisValuesByLineComparison,
|
|
gcodeExecutionProcessComparison,
|
|
sourceManifestComparison: strictComparison.sourceManifestComparison,
|
|
runtimeLaunchComparison: strictComparison.runtimeLaunchComparison,
|
|
halGraphComparison: strictComparison.halGraphComparison,
|
|
iniFullComparison: strictComparison.iniFullComparison,
|
|
kinematicsFormulaComparison: strictComparison.kinematicsFormulaComparison,
|
|
uiBehaviorComparison: strictComparison.axisUiBehaviorComparison,
|
|
visualComparison: strictComparison.visualComparison,
|
|
strictComparison,
|
|
requiredImprovements: [
|
|
...failed.map((item) => ({
|
|
category: item.category,
|
|
requirement: item.requirement,
|
|
evidence: item.evidence,
|
|
})),
|
|
...functionalSummary.requiredImprovements,
|
|
],
|
|
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 compareStrictEvidence(nativeEvidence, webEvidence) {
|
|
const nativeManifestFiles = Array.isArray(nativeEvidence.sourceManifest?.files)
|
|
? nativeEvidence.sourceManifest.files
|
|
: [];
|
|
const webManifestFiles = Array.isArray(webEvidence.sourceManifest?.files)
|
|
? webEvidence.sourceManifest.files
|
|
: [];
|
|
const nativeByRel = new Map(nativeManifestFiles.map((file) => [file.sourceRel, file]));
|
|
const stagedCommon = webManifestFiles
|
|
.filter((file) => nativeByRel.has(file.sourceRel))
|
|
.map((file) => ({
|
|
sourceRel: file.sourceRel,
|
|
nativeSha256: nativeByRel.get(file.sourceRel)?.sha256 || null,
|
|
webSha256: file.sha256 || null,
|
|
match: nativeByRel.get(file.sourceRel)?.sha256 === file.sha256,
|
|
}));
|
|
const hashMismatches = stagedCommon.filter((item) => !item.match);
|
|
const pathStatsStrict = pathComparison.semanticExecutionVsSemanticExecution || {};
|
|
const strict = {
|
|
sourceManifestComparison: statusObject(
|
|
nativeEvidence.sourceManifest?.ready === true
|
|
&& webEvidence.sourceManifest?.ready === true
|
|
&& nativeEvidence.sourceManifest?.missingCount === 0
|
|
&& nativeManifestFiles.length >= 20,
|
|
{
|
|
nativeFileCount: nativeManifestFiles.length,
|
|
nativeMissingCount: nativeEvidence.sourceManifest?.missingCount,
|
|
webFileCount: webManifestFiles.length,
|
|
nativeRoles: nativeEvidence.sourceManifest?.roles,
|
|
},
|
|
),
|
|
runtimeLaunchComparison: statusObject(
|
|
nativeEvidence.runtimeLaunch?.ready === true
|
|
&& webEvidence.runtimeLaunch?.ready === true
|
|
&& Boolean(nativeEvidence.runtimeLaunch?.entrypoints?.linuxcnc)
|
|
&& Boolean(webEvidence.runtimeLaunch?.entrypoints?.app),
|
|
{
|
|
nativeEntrypoints: nativeEvidence.runtimeLaunch?.entrypoints,
|
|
webEntrypoints: webEvidence.runtimeLaunch?.entrypoints,
|
|
},
|
|
),
|
|
iniFullComparison: statusObject(
|
|
nativeEvidence.iniFull?.ready === true
|
|
&& webEvidence.iniFull?.ready === true
|
|
&& nativeEvidence.iniFull?.sectionCount === webEvidence.iniFull?.sectionCount
|
|
&& nativeEvidence.iniFull?.keyCount === webEvidence.iniFull?.keyCount,
|
|
{
|
|
nativeSectionCount: nativeEvidence.iniFull?.sectionCount,
|
|
webSectionCount: webEvidence.iniFull?.sectionCount,
|
|
nativeKeyCount: nativeEvidence.iniFull?.keyCount,
|
|
webKeyCount: webEvidence.iniFull?.keyCount,
|
|
},
|
|
),
|
|
halGraphComparison: statusObject(
|
|
nativeEvidence.halGraph?.ready === true
|
|
&& webEvidence.halGraph?.ready === true
|
|
&& (nativeEvidence.halGraph?.commandCount || 0) > 0
|
|
&& (webEvidence.halGraph?.commandCount || 0) > 0,
|
|
{
|
|
nativeCommandCount: nativeEvidence.halGraph?.commandCount,
|
|
webCommandCount: webEvidence.halGraph?.commandCount,
|
|
nativeRuntimePins: nativeEvidence.halGraph?.runtimeObservedPins?.length,
|
|
webRuntimePins: webEvidence.halGraph?.runtimeObservedPins?.length,
|
|
},
|
|
),
|
|
kinematicsFormulaComparison: readyPair(nativeEvidence.kinematicsFormula, webEvidence.kinematicsFormula),
|
|
remapSemanticsComparison: readyPair(nativeEvidence.remapSemantics, webEvidence.remapSemantics),
|
|
pyvcpPostguiComparison: readyPair(nativeEvidence.pyvcpPostgui, webEvidence.pyvcpPostgui),
|
|
axisUiBehaviorComparison: readyPair(nativeEvidence.axisUiSource, webEvidence.axisUiSource),
|
|
visualComparison: readyPair(nativeEvidence.vismachStrict, webEvidence.vismachStrict),
|
|
servoTaskTimingComparison: statusObject(
|
|
nativeEvidence.servoTaskTiming?.ready === true
|
|
&& webEvidence.servoTaskTiming?.ready === true
|
|
&& nativeEvidence.servoTaskTiming?.samplePeriodMs === 50
|
|
&& webEvidence.servoTaskTiming?.samplePeriodMs === 50,
|
|
{
|
|
native: nativeEvidence.servoTaskTiming,
|
|
web: webEvidence.servoTaskTiming,
|
|
},
|
|
),
|
|
runtimeExecutionComparison: statusObject(
|
|
nativeEvidence.runtimeExecutionObserved?.runtimeSampled === true
|
|
&& webEvidence.runtimeExecutionObserved?.runtimeSampled === true
|
|
&& nativeEvidence.runtimeEvidenceClassification?.ready === true
|
|
&& webEvidence.runtimeEvidenceClassification?.ready === true,
|
|
{
|
|
nativeRuntimeStatus: nativeEvidence.runtimeExecutionObserved?.runtimeStatus,
|
|
nativeRuntimeEventCount: nativeEvidence.runtimeExecutionObserved?.runtimeEventCount,
|
|
webRuntimeSampleCount: webEvidence.runtimeExecutionObserved?.runtimeSampleCount,
|
|
nativeSourceDerivedSampleCount: nativeEvidence.runtimeExecutionObserved?.sourceDerivedSampleCount,
|
|
webSourceDerivedSampleCount: webEvidence.runtimeExecutionObserved?.sourceDerivedSampleCount,
|
|
},
|
|
),
|
|
webStagingHashComparison: statusObject(
|
|
webEvidence.webStagingHashParity?.ready === true
|
|
&& stagedCommon.length >= 10
|
|
&& hashMismatches.length === 0,
|
|
{
|
|
commonFileCount: stagedCommon.length,
|
|
mismatchCount: hashMismatches.length,
|
|
mismatches: hashMismatches.slice(0, 10),
|
|
},
|
|
),
|
|
wasmSourceBindingComparison: statusObject(
|
|
webEvidence.wasmSourceBinding?.ready === true
|
|
&& Array.isArray(webEvidence.wasmSourceBinding?.artifacts)
|
|
&& webEvidence.wasmSourceBinding.artifacts.every((item) => item.sha256),
|
|
{
|
|
artifactCount: webEvidence.wasmSourceBinding?.artifacts?.length || 0,
|
|
sourceManifestSha256: webEvidence.wasmSourceBinding?.sourceManifestSha256,
|
|
},
|
|
),
|
|
taskHalFullStateComparison: readyPair(nativeEvidence.taskHalFullState, webEvidence.taskHalFullState),
|
|
limitInterlocksComparison: readyPair(nativeEvidence.limitInterlocks, webEvidence.limitInterlocks),
|
|
toolParameterComparison: readyPair(nativeEvidence.toolParameterPersistence, webEvidence.toolParameterPersistence),
|
|
programCorpusComparison: readyPair(nativeEvidence.programCorpusExecution, webEvidence.programCorpusExecution),
|
|
nativeWebVisualComparison: readyPair(nativeEvidence.visualEvidence, webEvidence.visualEvidence),
|
|
errorPathComparison: readyPair(nativeEvidence.errorPathParity, webEvidence.errorPathParity),
|
|
dualBaselineComparison: statusObject(true, {
|
|
sections: [
|
|
"sourceManifestComparison",
|
|
"runtimeLaunchComparison",
|
|
"halGraphComparison",
|
|
"iniFullComparison",
|
|
"kinematicsFormulaComparison",
|
|
"uiBehaviorComparison",
|
|
"visualComparison",
|
|
],
|
|
}),
|
|
evidenceClassificationComparison: statusObject(
|
|
nativeEvidence.runtimeEvidenceClassification?.ready === true
|
|
&& webEvidence.runtimeEvidenceClassification?.ready === true
|
|
&& nativeEvidence.runtimeEvidenceClassification?.runtimeSampled?.includes("commandResult.events")
|
|
&& webEvidence.runtimeEvidenceClassification?.runtimeSampled?.includes("executionPath.samples"),
|
|
{
|
|
native: nativeEvidence.runtimeEvidenceClassification,
|
|
web: webEvidence.runtimeEvidenceClassification,
|
|
},
|
|
),
|
|
rerunEntryComparison: statusObject(
|
|
Boolean(nativeEvidence.strictAcceptance?.rerunCommand)
|
|
|| Boolean(webEvidence.strictAcceptance?.evidenceFiles?.length),
|
|
{
|
|
nativeRerunCommand: nativeEvidence.strictAcceptance?.rerunCommand,
|
|
webEvidenceFiles: webEvidence.strictAcceptance?.evidenceFiles,
|
|
},
|
|
),
|
|
reverseSourceIndexComparison: readyPair(nativeEvidence.reverseSourceIndex, webEvidence.reverseSourceIndex),
|
|
performanceBudgetComparison: statusObject(
|
|
nativeEvidence.performanceBudget?.ready === true
|
|
&& webEvidence.performanceBudget?.ready === true
|
|
&& (pathStatsStrict.maxTcpErrorMm ?? 0) <= (nativeEvidence.performanceBudget?.maxTcpErrorMmBudget ?? 0.001)
|
|
&& (pathStatsStrict.maxJointError ?? 0) <= (nativeEvidence.performanceBudget?.maxJointErrorBudget ?? 0.001)
|
|
&& (pathStatsStrict.maxToolAxisAngleDeg ?? 0) <= (nativeEvidence.performanceBudget?.maxToolAxisAngleDegBudget ?? 0.001)
|
|
&& (pathStatsStrict.sampleCountDelta ?? 0) <= (nativeEvidence.performanceBudget?.sampleLossBudget ?? 0),
|
|
{
|
|
maxTcpErrorMm: pathStatsStrict.maxTcpErrorMm,
|
|
maxJointError: pathStatsStrict.maxJointError,
|
|
maxToolAxisAngleDeg: pathStatsStrict.maxToolAxisAngleDeg,
|
|
sampleCountDelta: pathStatsStrict.sampleCountDelta,
|
|
},
|
|
),
|
|
strictAcceptanceComparison: readyPair(nativeEvidence.strictAcceptance, webEvidence.strictAcceptance),
|
|
};
|
|
return strict;
|
|
}
|
|
|
|
function readyPair(nativeItem, webItem) {
|
|
return statusObject(nativeItem?.ready === true && webItem?.ready === true, {
|
|
nativeReady: nativeItem?.ready,
|
|
webReady: webItem?.ready,
|
|
nativeBoundary: nativeItem?.semanticBoundary,
|
|
webBoundary: webItem?.semanticBoundary,
|
|
});
|
|
}
|
|
|
|
function statusObject(passed, evidence = {}) {
|
|
return {
|
|
status: passed ? "pass" : "fail",
|
|
...evidence,
|
|
};
|
|
}
|
|
|
|
function compareFunctionalEvidence(nativeEvidence, webEvidence, {
|
|
pathComparison,
|
|
lineExecutionComparison,
|
|
axisValuesByLineComparison,
|
|
gcodeExecutionProcessComparison,
|
|
strictComparison,
|
|
}) {
|
|
const nativeErrors = errorPathIds(nativeEvidence);
|
|
const webErrors = errorPathIds(webEvidence);
|
|
const requiredIllegalPaths = [
|
|
"run-while-estop",
|
|
"run-before-homed",
|
|
"wrong-mode",
|
|
"missing-file",
|
|
"bad-switchkins-type",
|
|
"remap-stop",
|
|
];
|
|
const taskPolicy = webEvidence.taskHalEquivalence?.taskPolicy
|
|
|| webEvidence.nativeStateFlowReview?.buttonInterlocks
|
|
|| {};
|
|
const webExecution = webEvidence.taskHalEquivalence?.taskHal || {};
|
|
const webBasicSim = webEvidence.basicSimEquivalent || webEvidence.taskHalEquivalence?.basicSimEquivalent || {};
|
|
const semanticPath = pathComparison.semanticExecutionVsSemanticExecution || {};
|
|
const runtimeReady = strictComparison.runtimeExecutionComparison?.status === "pass";
|
|
const taskHalReady = strictComparison.taskHalFullStateComparison?.status === "pass";
|
|
const sourceUiReady = strictComparison.axisUiBehaviorComparison?.status === "pass";
|
|
const errorReady = strictComparison.errorPathComparison?.status === "pass";
|
|
|
|
return [
|
|
check("functional", "cppTaskStateMachineParity", taskHalReady
|
|
&& webEvidence.taskHalEquivalence?.ready === true
|
|
&& webEvidence.nativeStateFlowReview?.ready === true
|
|
&& ["estop", "estop-reset", "on"].includes(String(taskPolicy.taskState || ""))
|
|
&& ["manual", "auto", "mdi"].includes(String(taskPolicy.taskMode || ""))
|
|
&& ["idle", "reading", "paused", "waiting"].includes(String(taskPolicy.interpState || "")), {
|
|
linuxCncRules: ["emcTaskSetState", "determineState", "EMC_STAT.task"],
|
|
nativeReady: nativeEvidence.taskHalFullState?.ready,
|
|
webReady: webEvidence.taskHalFullState?.ready,
|
|
taskPolicy,
|
|
}),
|
|
check("functional", "cppModeGateParity", sourceUiReady
|
|
&& errorReady
|
|
&& nativeErrors.has("wrong-mode")
|
|
&& webErrors.has("wrong-mode")
|
|
&& taskPolicy.canExecuteMdi === false
|
|
&& taskPolicy.canRunAuto === false, {
|
|
linuxCncRules: ["emcTaskSetMode", "AUTO non-idle/manual gate", "AXIS manual_ok/mdi gate"],
|
|
nativeWrongModeRepresented: nativeErrors.has("wrong-mode"),
|
|
webWrongModeRepresented: webErrors.has("wrong-mode"),
|
|
taskPolicy,
|
|
}),
|
|
check("functional", "cppHomingParity", taskHalReady
|
|
&& webBasicSim.ready === true
|
|
&& webBasicSim.homing?.allConfiguredAxesHomed === true
|
|
&& Array.isArray(nativeEvidence.taskStateFlow?.states)
|
|
&& nativeEvidence.taskStateFlow.states.some((state) => Array.isArray(state.homed)), {
|
|
linuxCncRules: ["EMC_JOINT_HOME", "EMCMOT_JOINT_HOME", "homing.c HOME_START/HOME_FINISHED"],
|
|
nativeTaskStateFlowReady: nativeEvidence.taskStateFlow?.ready,
|
|
webHoming: webBasicSim.homing,
|
|
}),
|
|
check("functional", "cppRunGateParity", taskHalReady
|
|
&& errorReady
|
|
&& nativeErrors.has("run-while-estop")
|
|
&& webErrors.has("run-while-estop")
|
|
&& nativeErrors.has("run-before-homed")
|
|
&& webErrors.has("run-before-homed")
|
|
&& webExecution.completed === true
|
|
&& webExecution.eventCount > 0
|
|
&& pathComparison.executionVsExecution.comparable === true, {
|
|
linuxCncRules: ["EMC_TASK_PLAN_RUN", "all_homed", "program open", "motion plan loaded"],
|
|
nativeErrorPaths: [...nativeErrors],
|
|
webErrorPaths: [...webErrors],
|
|
webExecution,
|
|
executionPath: {
|
|
nativeSampleCount: pathComparison.executionVsExecution.nativeSampleCount,
|
|
webSampleCount: pathComparison.executionVsExecution.webSampleCount,
|
|
},
|
|
}),
|
|
check("functional", "cppPauseResumeParity", taskHalReady
|
|
&& webExecution.eventCount > 0
|
|
&& typeof taskPolicy.canPause === "boolean"
|
|
&& typeof taskPolicy.canResume === "boolean"
|
|
&& semanticPath.status === "pass"
|
|
&& semanticPath.machineStateMismatchCount === 0, {
|
|
linuxCncRules: ["EMC_TASK_PLAN_PAUSE", "EMC_TASK_PLAN_RESUME", "EMCMOT_PAUSE", "EMCMOT_RESUME"],
|
|
taskPolicy: {
|
|
canPause: taskPolicy.canPause,
|
|
canResume: taskPolicy.canResume,
|
|
interpState: taskPolicy.interpState,
|
|
},
|
|
semanticExecutionMachineStateMismatchCount: semanticPath.machineStateMismatchCount,
|
|
}),
|
|
check("functional", "cppStepParity", taskHalReady
|
|
&& typeof taskPolicy.canRunAuto === "boolean"
|
|
&& lineExecutionComparison.status === "pass"
|
|
&& axisValuesByLineComparison.status === "pass"
|
|
&& gcodeExecutionProcessComparison.status === "pass"
|
|
&& gcodeExecutionProcessComparison.nativeExecutionStepCount > 0
|
|
&& gcodeExecutionProcessComparison.webExecutionStepCount > 0, {
|
|
linuxCncRules: ["EMC_TASK_PLAN_STEP", "EMCMOT_STEP", "motion id/current line progression"],
|
|
lineExecution: {
|
|
nativeTraceCount: lineExecutionComparison.nativeTraceCount,
|
|
webTraceCount: lineExecutionComparison.webTraceCount,
|
|
},
|
|
gcodeExecution: {
|
|
nativeExecutionStepCount: gcodeExecutionProcessComparison.nativeExecutionStepCount,
|
|
webExecutionStepCount: gcodeExecutionProcessComparison.webExecutionStepCount,
|
|
},
|
|
}),
|
|
check("functional", "illegalCommandParity", errorReady
|
|
&& requiredIllegalPaths.every((id) => nativeErrors.has(id) && webErrors.has(id))
|
|
&& strictComparison.limitInterlocksComparison?.status === "pass", {
|
|
linuxCncRules: ["runtime command rejection", "UI policy gate consistency", "operator error parity"],
|
|
requiredIllegalPaths,
|
|
nativeErrorPaths: [...nativeErrors],
|
|
webErrorPaths: [...webErrors],
|
|
limitInterlocksStatus: strictComparison.limitInterlocksComparison?.status,
|
|
}),
|
|
check("functional", "realProgramExecutionParity", runtimeReady
|
|
&& pathComparison.semanticExecutionVsSemanticExecution.status === "pass"
|
|
&& lineExecutionComparison.status === "pass"
|
|
&& axisValuesByLineComparison.status === "pass"
|
|
&& gcodeExecutionProcessComparison.status === "pass", {
|
|
linuxCncRules: ["real G-code line/currentLine/motion/path/status parity"],
|
|
runtimeExecutionStatus: strictComparison.runtimeExecutionComparison?.status,
|
|
semanticExecution: {
|
|
nativeSampleCount: semanticPath.nativeSampleCount,
|
|
webSampleCount: semanticPath.webSampleCount,
|
|
maxTcpErrorMm: semanticPath.maxTcpErrorMm,
|
|
maxJointError: semanticPath.maxJointError,
|
|
maxToolAxisAngleDeg: semanticPath.maxToolAxisAngleDeg,
|
|
},
|
|
lineExecutionStatus: lineExecutionComparison.status,
|
|
axisValuesStatus: axisValuesByLineComparison.status,
|
|
gcodeExecutionStatus: gcodeExecutionProcessComparison.status,
|
|
}),
|
|
];
|
|
}
|
|
|
|
function errorPathIds(evidence) {
|
|
return new Set((evidence.errorPathParity?.paths || [])
|
|
.filter((item) => item?.nativeExpected !== false && item?.webRepresented !== false)
|
|
.map((item) => item.id)
|
|
.filter(Boolean));
|
|
}
|
|
|
|
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;
|
|
}
|