From 8e37c69bc4f1ac8c62280d792d3cd90c4375aa88 Mon Sep 17 00:00:00 2001 From: wangdequan Date: Tue, 16 Jun 2026 06:27:31 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20OPFS=20session=20readiness?= =?UTF-8?q?=20SDK=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PROJECT_COMPLETION_TRACKER.md | 4 +- text14.txt | 66 +++++++++++ wasm-port/docs/opfs-session-persistence.md | 8 ++ .../runtime/opfs/machine-session-readiness.js | 103 ++++++++++++++++++ wasm-port/runtime/sdk/README.md | 4 + wasm-port/runtime/sdk/src/index.js | 1 + .../docs/node/verify_opfs_session_docs.mjs | 2 + .../tests/opfs/node/verify_file_service.mjs | 58 ++++++++++ .../tests/sdk/node/verify_sdk_surface.mjs | 2 + 9 files changed, 246 insertions(+), 2 deletions(-) create mode 100644 wasm-port/runtime/opfs/machine-session-readiness.js diff --git a/PROJECT_COMPLETION_TRACKER.md b/PROJECT_COMPLETION_TRACKER.md index d2801eb..e05139c 100644 --- a/PROJECT_COMPLETION_TRACKER.md +++ b/PROJECT_COMPLETION_TRACKER.md @@ -42,8 +42,8 @@ The project is complete for the current scope when all of the following are true | Sim config inventory | Stable with skips | `executed=28`, `passed=28`, `skipped=131`, `unexpected_fail=0`; `docs/sim-configs-coverage-handoff.md` records the release-gate baseline. | | Native nc_files baseline | Stable | Last recorded Layer 1: `total 107`, `pass 101`, `expected_fail 6`, `unexpected_fail 0`. | | Native sim configs baseline | Stable | Last recorded Layer 2: `total 159`, `pass 151`, `expected_fail 8`, `unexpected_fail 0`. | -| OPFS/session persistence | Stable for current scope | Browser smoke passes, SDK re-exports OPFS/session helpers, and `docs/opfs-session-persistence.md` defines the project-level release gate. | -| SDK/API surface | Stable for current scope | SDK re-exports core, OPFS/session, sim-config staging, and INI panel shell/workflow overview helpers with a Node surface smoke. | +| OPFS/session persistence | Stable for current scope | Browser smoke passes, SDK re-exports OPFS/session helpers including `readMachineSessionReadiness()`, and `docs/opfs-session-persistence.md` defines the project-level release gate. | +| SDK/API surface | Stable for current scope | SDK re-exports core, OPFS/session readiness/load helpers, sim-config staging, and INI panel shell/workflow overview helpers with a Node surface smoke. | | Browser/UI workflow | Stable for current scope | Workflow overview embedding mount DOM closure is complete and documented for external shells. | | Workflow overview embedding | Stable for current scope | Report/display/render, DOM contract/readiness/renderer, mount result/display/render, mount DOM contract/readiness/renderer/wrapper exist. | | Host/runtime boundary proof | Stable blocked state | Host smoke passes; runtime families remain blocked and `docs/host-runtime-boundary-handoff.md` records host readiness, promotion blockers, dispatch, and evidence gates. | diff --git a/text14.txt b/text14.txt index 322f856..77e062f 100644 --- a/text14.txt +++ b/text14.txt @@ -1638,3 +1638,69 @@ project_release_gate=ok 若本批验证通过并提交,后续可以继续做低风险可维护性推进:为 project release gate 增加短文档索引或 CI 入口说明;或者在真实 LinuxCNC host runtime 可用前,保持 blocked runtime family 不 promotion, 只维护 guard、docs 和 workflow/API 边界。 + +二十、2026-06-16 继续执行记录:OPFS machine session readiness SDK helper + +本轮按“功能实质性推进为主”继续推进,新增可直接复用的 OPFS/session readiness helper。该 helper +允许外部 SDK/UI 调用方在真正加载 machine session 到 WASM 前,结构化检查 persisted INI、参数文件、 +工具表、可选 G-code 和可选 session snapshot 是否存在且 snapshot envelope 有效。 + +完成内容: + +- 新增 `wasm-port/runtime/opfs/machine-session-readiness.js`; +- 新增 `readMachineSessionReadiness(machineId, options)`; +- helper 返回: + - `machineId`; + - `ready`; + - `phase`; + - `paths`; + - `checks`; + - `snapshotCheck`; + - `snapshot`; + - `missing`; + - `errors`; +- 支持默认 machine file paths; +- 支持显式 `iniOpfsPath`、`parameterOpfsPath`、`toolTableOpfsPath`; +- 支持 `gcodeFilename`、`gcodeOpfsPath` 和 `gcodeRequired`; +- 支持 `sessionId` 与 `snapshotFilename`,并复用现有 `loadMachineSessionSnapshot()` 做 snapshot + envelope/machine id 校验; +- `wasm-port/runtime/sdk/src/index.js` re-export `readMachineSessionReadiness()`; +- `wasm-port/tests/opfs/node/verify_file_service.mjs` 覆盖 ready session、missing session、 + snapshot check、G-code check 与 invalid OPFS path; +- `wasm-port/tests/sdk/node/verify_sdk_surface.mjs` 覆盖 SDK export; +- `wasm-port/runtime/sdk/README.md` 记录新 SDK helper; +- `wasm-port/docs/opfs-session-persistence.md` 记录 readiness workflow; +- `wasm-port/tests/docs/node/verify_opfs_session_docs.mjs` 覆盖 docs drift; +- `PROJECT_COMPLETION_TRACKER.md` 同步 OPFS/session 与 SDK/API surface 状态; +- 未新增控制按钮; +- 未执行 action-plan; +- 未解析 G-code; +- 未解释 canonical events; +- 未新增 JS CNC 语义; +- 未运行 opt-in runtime probes; +- 未 promotion blocked runtime family; +- 未改变 LinuxCNC interpreter、tool、parameter、planner、kinematics、remap 或 LinuxCNC-owned + runtime semantics。 + +已通过的定向验证: + +```bash +git diff --check +wasm-port/tests/opfs/node/verify_file_service.sh +wasm-port/tests/sdk/node/verify_sdk_surface.sh +wasm-port/tests/docs/node/verify_opfs_session_docs.sh +``` + +关键输出: + +```text +opfs_file_service_node_smoke=ok +sdk_surface_node_smoke=ok +opfs_session_docs_node_smoke=ok +``` + +下一步工作计划: + +继续以实质能力推进为主。下一批建议把 `readMachineSessionReadiness()` 接入 INI panel/session +workflow 或 external shell handoff view-model,让 UI 在加载 session 前显示结构化 ready/blocked +原因;仍保持只读,不新增控制按钮,不解析 G-code,不扩大 LinuxCNC 语义边界。 diff --git a/wasm-port/docs/opfs-session-persistence.md b/wasm-port/docs/opfs-session-persistence.md index c290ab8..fb03058 100644 --- a/wasm-port/docs/opfs-session-persistence.md +++ b/wasm-port/docs/opfs-session-persistence.md @@ -31,6 +31,7 @@ Session snapshot helpers: - `loadSessionSnapshot()` - `loadMachineSessionSnapshot()` - `validateSessionSnapshot()` +- `readMachineSessionReadiness()` Machine file and program helpers: @@ -74,6 +75,11 @@ The browser/UI persistence workflow is host-side only: `restoreParameters()`, `saveParameters()`, `loadToolTable()`, and `saveToolTable()`. +External callers can run `readMachineSessionReadiness()` before step 4 to get a +structured `ready`/`blocked` report for persisted INI, parameter, tool-table, +optional G-code, and optional session snapshot files. This helper only checks +host file availability and snapshot envelope validity. + This workflow stores host files and stages them into Emscripten FS. It does not implement G-code, tool-table, parameter-file, planner, remap, or kinematics semantics in JavaScript. @@ -154,6 +160,8 @@ ENABLE_PYTHON_REMAP_RUNTIME_PROBE=1 bash wasm-port/tests/native/probe_python_rem - Invalid OPFS paths are rejected before storage access. - Invalid snapshot format, version, session id, payload, or machine id is rejected by snapshot validation helpers. +- `readMachineSessionReadiness()` reports missing files or invalid snapshots as + structured blocked checks before callers load a session into WASM. - Missing INI, parameter, or tool-table OPFS files remain host persistence errors. JavaScript does not synthesize LinuxCNC machine state. - Tool, parameter, remap, planner, and kinematics behavior remains owned by diff --git a/wasm-port/runtime/opfs/machine-session-readiness.js b/wasm-port/runtime/opfs/machine-session-readiness.js new file mode 100644 index 0000000..f78f0ba --- /dev/null +++ b/wasm-port/runtime/opfs/machine-session-readiness.js @@ -0,0 +1,103 @@ +import { loadTextFile } from "./file-service.js"; +import { + gcodeProgramPath, + machineIniPath, + normalizeOpfsPath, + parameterFilePath, + sessionSnapshotPath, + toolTablePath, +} from "./path-model.js"; +import { loadMachineSessionSnapshot } from "./snapshot-store.js"; + +function checkResult(name, path, ok, error = null) { + return { + name, + path, + ok, + error, + }; +} + +async function checkTextFile(name, path, storage) { + try { + const text = await loadTextFile(path, storage); + return { + ...checkResult(name, path, true), + bytes: text.length, + }; + } catch (error) { + return checkResult(name, path, false, error.message); + } +} + +function resolveReadinessPaths(machineId, options = {}) { + const paths = { + ini: options.iniOpfsPath ?? machineIniPath(machineId, options.iniFilename), + parameters: + options.parameterOpfsPath ?? parameterFilePath(machineId, options.parameterFilename), + toolTable: + options.toolTableOpfsPath ?? toolTablePath(machineId, options.toolTableFilename), + }; + + if (options.gcodeOpfsPath !== undefined) { + paths.gcode = normalizeOpfsPath(options.gcodeOpfsPath); + } else if (options.gcodeFilename !== undefined) { + paths.gcode = gcodeProgramPath(options.gcodeFilename); + } + + if (options.sessionId !== undefined) { + paths.snapshot = sessionSnapshotPath(options.sessionId, options.snapshotFilename); + } + + return paths; +} + +function summarizeReadiness(checks, snapshotResult) { + const failed = checks.filter((check) => !check.ok); + const snapshotFailed = snapshotResult && !snapshotResult.ok ? [snapshotResult] : []; + return [...failed, ...snapshotFailed]; +} + +export async function readMachineSessionReadiness(machineId, options = {}) { + const paths = resolveReadinessPaths(machineId, options); + const checks = [ + await checkTextFile("ini", paths.ini, options.storage), + await checkTextFile("parameters", paths.parameters, options.storage), + await checkTextFile("toolTable", paths.toolTable, options.storage), + ]; + + if (options.gcodeRequired === true || paths.gcode !== undefined) { + checks.push(await checkTextFile("gcode", paths.gcode, options.storage)); + } + + let snapshot = null; + let snapshotCheck = null; + if (options.sessionId !== undefined) { + try { + snapshot = await loadMachineSessionSnapshot(options.sessionId, machineId, { + storage: options.storage, + filename: options.snapshotFilename, + }); + snapshotCheck = checkResult("snapshot", paths.snapshot, true); + } catch (error) { + snapshotCheck = checkResult("snapshot", paths.snapshot, false, error.message); + } + } + + const failures = summarizeReadiness(checks, snapshotCheck); + return { + machineId, + ready: failures.length === 0, + phase: failures.length === 0 ? "ready" : "blocked", + paths, + checks, + snapshotCheck, + snapshot, + missing: failures.map((failure) => failure.name), + errors: failures.map((failure) => ({ + name: failure.name, + path: failure.path, + error: failure.error, + })), + }; +} diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index 6825fd7..e39b58b 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -49,6 +49,7 @@ import { parameterFilePath, planIniFileContextStaging, planSimConfigStaging, + readMachineSessionReadiness, renderIniPanelShellWorkflowOverviewEmbeddingMountState, restoreMachineParametersFromOpfs, saveMachineSessionSnapshot, @@ -171,6 +172,9 @@ private module paths: snapshot persistence. - `machineFilePaths()`, `saveMachineTextFiles()`, `loadMachineTextFiles()`, and `gcodeFilenameFromProgramPath()` for machine-file and G-code file helpers. +- `readMachineSessionReadiness()` for checking persisted INI, parameter, + tool-table, optional G-code, and optional session snapshot files before + loading a session. - `restoreMachineParametersFromOpfs()`, `loadMachineToolTableFromOpfs()`, and `loadMachineSessionFromOpfs()` for loading OPFS machine state into the LinuxCNC-backed interpreter SDK. diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js index d6af6e1..3bb1d61 100644 --- a/wasm-port/runtime/sdk/src/index.js +++ b/wasm-port/runtime/sdk/src/index.js @@ -51,6 +51,7 @@ export { saveMachineToolTableToOpfs, } from "../../opfs/linuxcnc-tool-table-bridge.js"; export { loadMachineSessionFromOpfs } from "../../opfs/linuxcnc-machine-session-bridge.js"; +export { readMachineSessionReadiness } from "../../opfs/machine-session-readiness.js"; export { createIniPanelLaunchApiManifest, diff --git a/wasm-port/tests/docs/node/verify_opfs_session_docs.mjs b/wasm-port/tests/docs/node/verify_opfs_session_docs.mjs index 6635e13..e8904d7 100644 --- a/wasm-port/tests/docs/node/verify_opfs_session_docs.mjs +++ b/wasm-port/tests/docs/node/verify_opfs_session_docs.mjs @@ -17,6 +17,7 @@ for (const phrase of [ "OPFS/session persistence and release gate", "runtime/sdk/src/index.js", "createMachineSessionSnapshotPayload", + "readMachineSessionReadiness", "loadMachineSessionFromOpfs", "restoreMachineParametersFromOpfs", "loadMachineToolTableFromOpfs", @@ -33,6 +34,7 @@ for (const phrase of [ assert.match(readmeText, /docs\/opfs-session-persistence\.md/); assert.match(sdkReadmeText, /OPFS\/session persistence exports/); +assert.match(sdkReadmeText, /readMachineSessionReadiness/); assert.match(hostSmokeText, /verify_sdk_surface\.sh/); assert.match(hostSmokeText, /verify_file_service\.sh/); assert.match(browserSmokeText, /browser_ini_opfs_smoke=ok/); diff --git a/wasm-port/tests/opfs/node/verify_file_service.mjs b/wasm-port/tests/opfs/node/verify_file_service.mjs index 9dd900c..610a8ee 100644 --- a/wasm-port/tests/opfs/node/verify_file_service.mjs +++ b/wasm-port/tests/opfs/node/verify_file_service.mjs @@ -43,6 +43,9 @@ import { import { loadMachineSessionFromOpfs, } from "../../../runtime/opfs/linuxcnc-machine-session-bridge.js"; +import { + readMachineSessionReadiness, +} from "../../../runtime/opfs/machine-session-readiness.js"; class MockFileHandle { constructor(name) { @@ -420,6 +423,54 @@ assertGcodeProgramStoragePath( ); assert.equal(await loadGcodeProgram("custom-fixture.ngc", { storage }), "G1 X1 F10\nM2\n"); +const readyMachineSession = await readMachineSessionReadiness("xyzab-tdr", { + storage, + sessionId: "machine-session-1", + snapshotFilename: "machine-session-snapshot.json", + gcodeFilename: "fixture.ngc", +}); +assert.equal(readyMachineSession.ready, true); +assert.equal(readyMachineSession.phase, "ready"); +assert.deepEqual(readyMachineSession.missing, []); +assert.deepEqual(readyMachineSession.errors, []); +assert.deepEqual( + readyMachineSession.checks.map((check) => [check.name, check.ok, check.path]), + [ + ["ini", true, "linuxcnc/machines/xyzab-tdr/machine.ini"], + ["parameters", true, "linuxcnc/machines/xyzab-tdr/linuxcnc.var"], + ["toolTable", true, "linuxcnc/machines/xyzab-tdr/tool.tbl"], + ["gcode", true, "linuxcnc/gcode/fixture.ngc"], + ], +); +assert.deepEqual(readyMachineSession.snapshotCheck, { + name: "snapshot", + path: "linuxcnc/sessions/machine-session-1/machine-session-snapshot.json", + ok: true, + error: null, +}); +assert.equal(readyMachineSession.snapshot.sessionId, "machine-session-1"); + +const missingMachineSession = await readMachineSessionReadiness("missing-machine", { + storage, + sessionId: "missing-session", + gcodeRequired: true, + gcodeFilename: "missing.ngc", +}); +assert.equal(missingMachineSession.ready, false); +assert.equal(missingMachineSession.phase, "blocked"); +assert.deepEqual(missingMachineSession.missing, [ + "ini", + "parameters", + "toolTable", + "gcode", + "snapshot", +]); +assert.deepEqual( + missingMachineSession.errors.map((error) => error.name), + ["ini", "parameters", "toolTable", "gcode", "snapshot"], +); +assert.match(missingMachineSession.errors[0].error, /missing directory|missing file/); + const bridgeFiles = new Map(); const bridgeInterp = { writeTextFile(path, text) { @@ -897,6 +948,13 @@ await assert.rejects( () => loadGcodeProgram("../escape.ngc", { storage }), /Invalid G-code filename/, ); +await assert.rejects( + () => readMachineSessionReadiness("xyzab-tdr", { + storage, + gcodeOpfsPath: "../escape.ngc", + }), + /Invalid OPFS path/, +); await assert.rejects( () => saveSessionSnapshot("session-1", {}, { storage, diff --git a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs index f5099f7..cbf073f 100644 --- a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs +++ b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs @@ -30,6 +30,7 @@ import { parameterFilePath, planIniFileContextStaging, planSimConfigStaging, + readMachineSessionReadiness, renderIniPanelShellWorkflowOverviewEmbeddingMountState, restoreMachineParametersFromOpfs, saveMachineSessionSnapshot, @@ -76,6 +77,7 @@ const requiredExports = [ ["restoreMachineParametersFromOpfs", restoreMachineParametersFromOpfs], ["loadMachineToolTableFromOpfs", loadMachineToolTableFromOpfs], ["loadMachineSessionFromOpfs", loadMachineSessionFromOpfs], + ["readMachineSessionReadiness", readMachineSessionReadiness], ["createIniPanelLaunchApiManifest", createIniPanelLaunchApiManifest], ["isSupportedIniPanelLaunchApiManifest", isSupportedIniPanelLaunchApiManifest], ["createIniPanelShellViewModel", createIniPanelShellViewModel],