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; }