新增 OPFS session readiness SDK helper

This commit is contained in:
2026-06-16 06:27:31 +08:00
parent d076763ef4
commit 8e37c69bc4
9 changed files with 246 additions and 2 deletions

View File

@@ -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. |

View File

@@ -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 语义边界。

View File

@@ -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

View File

@@ -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,
})),
};
}

View File

@@ -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.

View File

@@ -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,

View File

@@ -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/);

View File

@@ -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,

View File

@@ -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],