diff --git a/PROJECT_COMPLETION_TRACKER.md b/PROJECT_COMPLETION_TRACKER.md index fe1cd57..8b8913e 100644 --- a/PROJECT_COMPLETION_TRACKER.md +++ b/PROJECT_COMPLETION_TRACKER.md @@ -9,7 +9,7 @@ turn-by-turn continuation notes and next-batch execution records. ## Current Status - Active continuation file: `text14.txt` -- Latest completed batch: project release readiness gate artifact +- Latest completed batch: project release readiness artifact validation workflow - Latest relevant commit: use `git log -1 --oneline` after each committed batch - Working tree at tracker creation: clean - Required policy: LinuxCNC remains the only CNC semantic source @@ -43,12 +43,12 @@ The project is complete for the current scope when all of the following are true | 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 including `readMachineSessionReadiness()` and `createMachineSessionPersistenceSummary()`, the UI exposes `getMachineSessionPersistenceSummary()`, 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, project release readiness, INI panel entry/launcher/control-page/shell/workflow overview helpers, the session readiness workflow report, and the shell API surface inventory with a Node surface smoke. | +| SDK/API surface | Stable for current scope | SDK re-exports core, OPFS/session readiness/load helpers, sim-config staging, project release readiness report/artifact validation, INI panel entry/launcher/control-page/shell/workflow overview helpers, the session readiness workflow report, and the shell API surface inventory with a Node surface smoke. | | Browser/UI workflow | Stable for current scope | Workflow overview embedding mount DOM closure is complete, and INI panel session workflows plus read-only control/shell handoff expose, render, and report OPFS/session readiness, OPFS/session persistence summary, and API surface inventory before loading or reading WASM session state. | | 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. | | Documentation/release gate | Stable for current scope | `docs/project-release-handoff.md` links README, panel entry, SDK, OPFS/session, sim-config, host/runtime, source reuse, drift, tracker acceptance paths, and the machine-readable release readiness report/artifact. | -| Project release gate wrapper | Stable | `tests/host/verify_project_release_gate.sh` runs the documented minimum release validation commands, writes `build/project-release-readiness.json`, verifies it, and ends with `project_release_gate=ok`. | +| Project release gate wrapper | Stable | `tests/host/verify_project_release_gate.sh` runs the documented minimum release validation commands, writes `build/project-release-readiness.json`, verifies it through the SDK artifact validation workflow, and ends with `project_release_gate=ok`. | ## Blocked Runtime Families diff --git a/text14.txt b/text14.txt index 29f0ab0..51a8bc0 100644 --- a/text14.txt +++ b/text14.txt @@ -2918,3 +2918,85 @@ artifact 摘要: 继续只做实质能力推进。下一批优先选择真实 browser/UI workflow 或外部 shell 可消费能力; 如果继续 release 路径,必须产出可执行 gate 或可消费 artifact,不再只做文档/记录补强。 + +三十七、2026-06-16 继续执行记录:project release readiness artifact validation workflow + +本轮继续按“每批必须产出可调用 API、可验证 workflow、可执行 gate、或真实 UI/browser 能力”推进, +把 release readiness artifact 从 host 测试里的硬编码断言推进为 SDK 可调用的 artifact parse/validation +workflow。外部工具现在可以直接复用 SDK 校验 `build/project-release-readiness.json`,host gate verifier +也改为复用同一套 SDK workflow。 + +完成内容: + +- `runtime/sdk/src/project-release-readiness.js` 新增: + - `parseProjectReleaseReadinessArtifactJson()`; + - `createProjectReleaseReadinessArtifactValidation()`; +- artifact validation 输出: + - `apiName: "project-release-readiness-artifact-validation"`; + - `validationVersion`; + - `phase`; + - `ready`; + - `missing`; + - `artifactApiName`; + - `artifactVersion`; + - `gateCount`; + - `blockedRuntimeFamilies`; + - `rows`; +- 修复 artifact validation 中空 `gates` 数组会因 `Array.every()` 空真值被误判为 passed 的问题; +- `runtime/sdk/src/index.js` re-export 新 parse/validation API; +- `tests/host/verify_project_release_readiness_artifact.mjs` 改为调用 SDK parse/validation workflow; +- `verify_sdk_surface.mjs` 覆盖: + - artifact JSON parse; + - ready artifact validation; + - 空 artifact blocked/missing; + - blocked runtime family 不被误 promotion; +- `runtime/sdk/README.md` 和 `docs/project-release-handoff.md` 记录外部工具如何复用 artifact validation API; +- `verify_project_release_handoff_docs.mjs` 覆盖新 artifact validation API; +- `PROJECT_COMPLETION_TRACKER.md` 同步 latest completed batch、SDK/API surface 与 release gate wrapper 状态; +- 未新增控制按钮; +- 未执行 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/sdk/node/verify_sdk_surface.sh +wasm-port/tests/host/verify_project_release_readiness_artifact.sh +wasm-port/tests/docs/node/verify_project_release_handoff_docs.sh +wasm-port/tools/verify_vendor_sync.sh +wasm-port/tools/verify_no_standalone_cnc_semantics.sh +SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh +SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh +wasm-port/tests/ui/node/verify_ui_node_smokes.sh +SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh +wasm-port/tests/host/verify_project_release_gate.sh +``` + +关键输出: + +```text +sdk_surface_node_smoke=ok +project_release_readiness_artifact_node_smoke=ok +project_release_handoff_docs_node_smoke=ok +vendor sync up to date +standalone CNC semantics guard complete +interp_wasm_node_smoke=ok +sim_configs_wasm_node_inventory_unexpected_fail=0 +ui_node_smokes=ok +browser_ini_shell_integration_workflow_smoke=ok +project_release_readiness_artifact=/home/cnc/桌面/cnc_wams/wasm-port/build/project-release-readiness.json +host_wasm_opfs_browser_smokes=ok +project_release_gate=ok +``` + +下一步工作计划: + +继续只做实质能力推进。优先选择真实 browser/UI workflow 或外部 shell 可消费能力; +如果继续 release 路径,必须继续产出可调用 API、可验证 workflow、可执行 gate 或可消费 artifact。 diff --git a/wasm-port/docs/project-release-handoff.md b/wasm-port/docs/project-release-handoff.md index 2fd34f7..d8b1516 100644 --- a/wasm-port/docs/project-release-handoff.md +++ b/wasm-port/docs/project-release-handoff.md @@ -41,7 +41,9 @@ continuation records are in `../text14.txt`. evidence driven and only reports `ready: true` when the caller supplies release gate evidence such as `project_release_gate=ok`. The project release gate writes the same report to `build/project-release-readiness.json` and - validates it with `verify_project_release_readiness_artifact.sh`. + validates it with `verify_project_release_readiness_artifact.sh`. External + tools can load that artifact with `parseProjectReleaseReadinessArtifactJson()` + and validate it with `createProjectReleaseReadinessArtifactValidation()`. - OPFS/session callers can use `createMachineSessionPersistenceSummary()` or `linuxCncIniPanelApi.getMachineSessionPersistenceSummary()` to inspect machine files, session snapshot, session readiness, session load, readonly diff --git a/wasm-port/runtime/sdk/README.md b/wasm-port/runtime/sdk/README.md index c52257b..b91fcd0 100644 --- a/wasm-port/runtime/sdk/README.md +++ b/wasm-port/runtime/sdk/README.md @@ -47,6 +47,7 @@ import { createMachineSessionPersistenceRenderState, createMachineSessionPersistenceSummary, createMachineSessionSnapshotPayload, + createProjectReleaseReadinessArtifactValidation, createProjectReleaseReadinessReport, createSessionSnapshot, defaultMachinePaths, @@ -69,6 +70,7 @@ import { mountIniPanelShellWorkflowOverviewEmbeddingMountState, normalizeOpfsPath, parameterFilePath, + parseProjectReleaseReadinessArtifactJson, planIniFileContextStaging, planSimConfigStaging, readMachineSessionReadiness, @@ -194,7 +196,10 @@ and `../PROJECT_COMPLETION_TRACKER.md`. The executable release gate uses the same helper to write `build/project-release-readiness.json` after all project checks pass, then validates that artifact with -`tests/host/verify_project_release_readiness_artifact.sh`. +`tests/host/verify_project_release_readiness_artifact.sh`. External tools can +load that JSON with `parseProjectReleaseReadinessArtifactJson()` and validate +it with `createProjectReleaseReadinessArtifactValidation()` without copying the +host test assertions. ## OPFS/session persistence exports diff --git a/wasm-port/runtime/sdk/src/index.js b/wasm-port/runtime/sdk/src/index.js index 46b02c8..1a13328 100644 --- a/wasm-port/runtime/sdk/src/index.js +++ b/wasm-port/runtime/sdk/src/index.js @@ -1,6 +1,10 @@ export { createLinuxCncIniSdk } from "./linuxcnc-ini.js"; export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js"; -export { createProjectReleaseReadinessReport } from "./project-release-readiness.js"; +export { + createProjectReleaseReadinessArtifactValidation, + createProjectReleaseReadinessReport, + parseProjectReleaseReadinessArtifactJson, +} from "./project-release-readiness.js"; export { analyzeIniRuntimeBoundaries, planIniFileContextStaging, diff --git a/wasm-port/runtime/sdk/src/project-release-readiness.js b/wasm-port/runtime/sdk/src/project-release-readiness.js index 9a9588e..91ad417 100644 --- a/wasm-port/runtime/sdk/src/project-release-readiness.js +++ b/wasm-port/runtime/sdk/src/project-release-readiness.js @@ -67,6 +67,9 @@ const BLOCKED_RUNTIME_FAMILIES = [ "L4-PYTHON-REMAP", ]; +const PROJECT_RELEASE_READINESS_ARTIFACT_API = "project-release-readiness-report"; +const PROJECT_RELEASE_READINESS_ARTIFACT_VERSION = 1; + function outputContains(observedOutputs, expectedOutput) { return observedOutputs.some((output) => String(output).includes(expectedOutput)); } @@ -81,6 +84,14 @@ function readGatePassed({ gate, gateResults, observedOutputs }) { return false; } +function objectOrEmpty(value) { + return value && typeof value === "object" ? value : {}; +} + +function arrayOrEmpty(value) { + return Array.isArray(value) ? value : []; +} + export function createProjectReleaseReadinessReport({ gateResults = {}, observedOutputs = [], @@ -118,8 +129,8 @@ export function createProjectReleaseReadinessReport({ ]; return { - apiName: "project-release-readiness-report", - reportVersion: 1, + apiName: PROJECT_RELEASE_READINESS_ARTIFACT_API, + reportVersion: PROJECT_RELEASE_READINESS_ARTIFACT_VERSION, phase: ready ? "ready" : "waiting", ready, missing, @@ -152,3 +163,69 @@ export function createProjectReleaseReadinessReport({ ], }; } + +export function parseProjectReleaseReadinessArtifactJson(text) { + return JSON.parse(String(text)); +} + +export function createProjectReleaseReadinessArtifactValidation(artifact = {}) { + const artifactObject = objectOrEmpty(artifact); + const releaseGate = objectOrEmpty(artifactObject.releaseGate); + const simConfigInventory = objectOrEmpty(artifactObject.simConfigInventory); + const gates = arrayOrEmpty(artifactObject.gates); + const rows = arrayOrEmpty(artifactObject.rows); + const gateCountReady = gates.length === REQUIRED_RELEASE_GATES.length; + const gatesPassed = gateCountReady && gates.every(({ passed }) => passed === true); + const missing = [ + ...(artifactObject.apiName === PROJECT_RELEASE_READINESS_ARTIFACT_API ? [] : ["apiName"]), + ...(artifactObject.reportVersion === PROJECT_RELEASE_READINESS_ARTIFACT_VERSION ? [] : ["reportVersion"]), + ...(artifactObject.phase === "ready" ? [] : ["phase"]), + ...(artifactObject.ready === true ? [] : ["ready"]), + ...(Array.isArray(artifactObject.missing) && artifactObject.missing.length === 0 ? [] : ["missing"]), + ...(releaseGate.command === "wasm-port/tests/host/verify_project_release_gate.sh" ? [] : ["releaseGate.command"]), + ...(releaseGate.passed === true ? [] : ["releaseGate.passed"]), + ...(releaseGate.expectedOutput === "project_release_gate=ok" ? [] : ["releaseGate.expectedOutput"]), + ...(simConfigInventory.executed === 28 ? [] : ["simConfigInventory.executed"]), + ...(simConfigInventory.passed === 28 ? [] : ["simConfigInventory.passed"]), + ...(simConfigInventory.skipped === 131 ? [] : ["simConfigInventory.skipped"]), + ...(simConfigInventory.unexpectedFail === 0 ? [] : ["simConfigInventory.unexpectedFail"]), + ...(arrayOrEmpty(artifactObject.blockedRuntimeFamilies).join(",") === BLOCKED_RUNTIME_FAMILIES.join(",") + ? [] + : ["blockedRuntimeFamilies"]), + ...(arrayOrEmpty(artifactObject.promotedBlockedFamilies).length === 0 ? [] : ["promotedBlockedFamilies"]), + ...(gateCountReady ? [] : ["gates.length"]), + ...(gatesPassed ? [] : ["gates.passed"]), + ...(rows.find(({ id, value }) => id === "project-release-gate" && value === "passed") + ? [] + : ["rows.project-release-gate"]), + ]; + + return { + apiName: "project-release-readiness-artifact-validation", + validationVersion: 1, + phase: missing.length === 0 ? "ready" : "blocked", + ready: missing.length === 0, + missing, + artifactApiName: artifactObject.apiName ?? null, + artifactVersion: artifactObject.reportVersion ?? null, + gateCount: gates.length, + blockedRuntimeFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies), + rows: [ + { + id: "artifact", + label: "Readiness artifact", + value: artifactObject.apiName ?? "missing", + }, + { + id: "project-release-gate", + label: "Project release gate", + value: releaseGate.passed === true ? "passed" : "missing", + }, + { + id: "validation", + label: "Artifact validation", + value: missing.length === 0 ? "ready" : missing.join(", "), + }, + ], + }; +} diff --git a/wasm-port/tests/docs/node/verify_project_release_handoff_docs.mjs b/wasm-port/tests/docs/node/verify_project_release_handoff_docs.mjs index 1280854..96464c3 100644 --- a/wasm-port/tests/docs/node/verify_project_release_handoff_docs.mjs +++ b/wasm-port/tests/docs/node/verify_project_release_handoff_docs.mjs @@ -32,6 +32,8 @@ for (const phrase of [ "PROJECT_COMPLETION_TRACKER.md", "runtime/sdk/src/index.js", "createProjectReleaseReadinessReport()", + "parseProjectReleaseReadinessArtifactJson()", + "createProjectReleaseReadinessArtifactValidation()", "createIniPanelShellApiSurfaceInventory()", "createIniPanelShellSessionReadinessWorkflowReport()", "workflow overview embedding mount DOM", @@ -89,6 +91,8 @@ for (const phrase of [ "OPFS/session persistence exports", "Project release readiness export", "createProjectReleaseReadinessReport()", + "parseProjectReleaseReadinessArtifactJson()", + "createProjectReleaseReadinessArtifactValidation()", "build/project-release-readiness.json", "INI panel shell handoff exports", "createIniPanelShellApiSurfaceInventory()", diff --git a/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs b/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs index 136febb..b50b1f1 100644 --- a/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs +++ b/wasm-port/tests/host/verify_project_release_readiness_artifact.mjs @@ -3,33 +3,23 @@ import { readFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; +import { + createProjectReleaseReadinessArtifactValidation, + parseProjectReleaseReadinessArtifactJson, +} from "../../runtime/sdk/src/index.js"; + const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, "../.."); const artifactPath = process.argv[2] ?? resolve(root, "build/project-release-readiness.json"); -const artifact = JSON.parse(readFileSync(artifactPath, "utf8")); +const artifact = parseProjectReleaseReadinessArtifactJson(readFileSync(artifactPath, "utf8")); +const validation = createProjectReleaseReadinessArtifactValidation(artifact); -assert.equal(artifact.apiName, "project-release-readiness-report"); -assert.equal(artifact.reportVersion, 1); -assert.equal(artifact.phase, "ready"); -assert.equal(artifact.ready, true); -assert.deepEqual(artifact.missing, []); -assert.equal(artifact.releaseGate.command, "wasm-port/tests/host/verify_project_release_gate.sh"); -assert.equal(artifact.releaseGate.passed, true); -assert.equal(artifact.releaseGate.expectedOutput, "project_release_gate=ok"); -assert.equal(artifact.simConfigInventory.executed, 28); -assert.equal(artifact.simConfigInventory.passed, 28); -assert.equal(artifact.simConfigInventory.skipped, 131); -assert.equal(artifact.simConfigInventory.unexpectedFail, 0); -assert.deepEqual(artifact.blockedRuntimeFamilies, [ - "L4-USER-M-PROCESS", - "L4-TOOL-DB", - "L4-PYTHON-REMAP", -]); -assert.deepEqual(artifact.promotedBlockedFamilies, []); -assert.equal(artifact.gates.every(({ passed }) => passed === true), true); -assert.equal( - artifact.rows.find(({ id }) => id === "project-release-gate")?.value, - "passed", -); +assert.equal(validation.apiName, "project-release-readiness-artifact-validation"); +assert.equal(validation.ready, true); +assert.equal(validation.phase, "ready"); +assert.deepEqual(validation.missing, []); +assert.equal(validation.artifactApiName, "project-release-readiness-report"); +assert.equal(validation.artifactVersion, 1); +assert.equal(validation.gateCount, 9); console.log("project_release_readiness_artifact_node_smoke=ok"); diff --git a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs index 1c173eb..8b97ccd 100644 --- a/wasm-port/tests/sdk/node/verify_sdk_surface.mjs +++ b/wasm-port/tests/sdk/node/verify_sdk_surface.mjs @@ -28,7 +28,9 @@ import { createMachineSessionPersistenceDisplayViewModel, createMachineSessionPersistenceRenderState, createMachineSessionPersistenceSummary, + createProjectReleaseReadinessArtifactValidation, createProjectReleaseReadinessReport, + parseProjectReleaseReadinessArtifactJson, createSessionSnapshot, defaultMachinePaths, gcodeFilenameFromProgramPath, @@ -73,7 +75,9 @@ const trackerText = readFileSync(resolve(root, "../PROJECT_COMPLETION_TRACKER.md const requiredExports = [ ["createLinuxCncIniSdk", createLinuxCncIniSdk], ["createLinuxCncInterpSdk", createLinuxCncInterpSdk], + ["createProjectReleaseReadinessArtifactValidation", createProjectReleaseReadinessArtifactValidation], ["createProjectReleaseReadinessReport", createProjectReleaseReadinessReport], + ["parseProjectReleaseReadinessArtifactJson", parseProjectReleaseReadinessArtifactJson], ["planIniFileContextStaging", planIniFileContextStaging], ["planSimConfigStaging", planSimConfigStaging], ["analyzeIniRuntimeBoundaries", analyzeIniRuntimeBoundaries], @@ -170,6 +174,73 @@ const releaseReadinessReady = createProjectReleaseReadinessReport({ }); assert.equal(releaseReadinessReady.ready, true); assert.equal(releaseReadinessReady.releaseGate.passed, true); +const releaseReadinessArtifact = parseProjectReleaseReadinessArtifactJson(JSON.stringify(createProjectReleaseReadinessReport({ + gateResults: { + "diff-check": true, + "vendor-sync": true, + "standalone-cnc-semantics": true, + "interp-wasm": true, + "sim-config-inventory": true, + "ini-panel-browser": true, + "ui-node-smokes": true, + "host-smokes": true, + "project-release-gate": true, + }, +}))); +assert.equal(releaseReadinessArtifact.apiName, "project-release-readiness-report"); +assert.deepEqual(createProjectReleaseReadinessArtifactValidation(releaseReadinessArtifact), { + apiName: "project-release-readiness-artifact-validation", + validationVersion: 1, + phase: "ready", + ready: true, + missing: [], + artifactApiName: "project-release-readiness-report", + artifactVersion: 1, + gateCount: 9, + blockedRuntimeFamilies: [ + "L4-USER-M-PROCESS", + "L4-TOOL-DB", + "L4-PYTHON-REMAP", + ], + rows: [ + { + id: "artifact", + label: "Readiness artifact", + value: "project-release-readiness-report", + }, + { + id: "project-release-gate", + label: "Project release gate", + value: "passed", + }, + { + id: "validation", + label: "Artifact validation", + value: "ready", + }, + ], +}); +assert.deepEqual( + createProjectReleaseReadinessArtifactValidation({}).missing, + [ + "apiName", + "reportVersion", + "phase", + "ready", + "missing", + "releaseGate.command", + "releaseGate.passed", + "releaseGate.expectedOutput", + "simConfigInventory.executed", + "simConfigInventory.passed", + "simConfigInventory.skipped", + "simConfigInventory.unexpectedFail", + "blockedRuntimeFamilies", + "gates.length", + "gates.passed", + "rows.project-release-gate", + ], +); assert.equal( createProjectReleaseReadinessReport({ observedOutputs: ["project_release_gate=ok"],